[
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04\n\nENV FLUTTER_CHANNEL=\"stable\"\nENV FLUTTER_VERSION=\"3.24.5\"\nENV FLUTTER_HOME=\"/home/vscode/flutter\"\nENV ANDROID_HOME=\"/home/vscode/.android-sdk\"\nENV ANDROID_USER_HOME=\"/home/vscode/.android\"\nENV ANDROID_PLATFORM_VERSION=\"36\"\nENV ANDROID_BUILD_TOOLS_VERSION=\"36.0.0\"\nENV ANDROID_SYSTEM_IMAGE=\"system-images;android-${ANDROID_PLATFORM_VERSION};google_apis;x86_64\"\nENV DEBIAN_FRONTEND=\"noninteractive\"\nENV PATH=${PATH}:${FLUTTER_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator\n\nRUN apt-get update && \\\n\tapt-get install -y --no-install-recommends \\\n\tclang \\\n\tcmake \\\n\tcurl \\\n\tdconf-gsettings-backend \\\n\tdconf-service \\\n\tgsettings-desktop-schemas \\\n\tlibcanberra-gtk-module \\\n\tlibgl1-mesa-dri \\\n\tlibglu1-mesa \\\n\tlibgtk-3-dev \\\n\tliblzma-dev \\\n\tlibpulse-dev \\\n\tlibsecret-1-dev \\\n\tlibsqlite3-dev \\\n\tlibstdc++-12-dev \\\n\tlibx11-xcb1 \\\n\tlibxcb-cursor0 \\\n\tlibxcb-xinerama0 \\\n\tlibxkbcommon-x11-0 \\\n\tlibxkbfile-dev \\\n\tninja-build \\\n\topenjdk-17-jdk-headless \\\n\tpkg-config \\\n\tunzip \\\n\tvim \\\n\twget \\\n\txdg-user-dirs \\\n\txz-utils \\\n\tzip && \\\n\tapt-get clean -y && \\\n\trm -rf /var/lib/apt/lists/*\n\nRUN mkdir -p ${ANDROID_USER_HOME} ${ANDROID_HOME}/cmdline-tools && \\\n\twget \"https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip\" -O commandlinetools.zip && \\\n\tunzip commandlinetools.zip -d ${ANDROID_HOME}/cmdline-tools && \\\n\tmv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest && \\\n\trm commandlinetools.zip\n\nRUN yes | sdkmanager --licenses && \\\n\tsdkmanager --update && \\\n\tsdkmanager platform-tools && \\\n\tsdkmanager emulator && \\\n\tsdkmanager \"platforms;android-${ANDROID_PLATFORM_VERSION}\" && \\\n\tsdkmanager \"build-tools;${ANDROID_BUILD_TOOLS_VERSION}\" && \\\n\tsdkmanager \"${ANDROID_SYSTEM_IMAGE}\" && \\\n\techo | avdmanager create avd -n android-${ANDROID_PLATFORM_VERSION} -k \"${ANDROID_SYSTEM_IMAGE}\"\n\nRUN chown -R vscode:vscode ${ANDROID_HOME} && \\\n\tchown -R vscode:vscode ${ANDROID_USER_HOME}\n\nRUN mkdir -p ${FLUTTER_HOME} && \\\n\twget \"https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz\" \\\n\t\t-O flutter.tar.xz && \\\n    tar xf flutter.tar.xz -C ${FLUTTER_HOME} --strip-components=1 && \\\n    rm flutter.tar.xz && \\\n\tchown -R vscode:vscode ${FLUTTER_HOME}\n\nUSER vscode\n\nRUN dart --disable-analytics && flutter --disable-analytics\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"name\": \"Flutter\",\n  \"dockerFile\": \"Dockerfile\",\n  \"remoteUser\": \"vscode\",\n  \"updateRemoteUserUID\": true,\n  \"runArgs\": [\"--device=/dev/dri\", \"--device=/dev/kvm\", \"--group-add=video\"],\n\n  \"features\": {\n    \"ghcr.io/devcontainers-extra/features/shellcheck:1\": {},\n    \"ghcr.io/devcontainers-extra/features/shfmt:1\": {},\n    \"ghcr.io/prulloac/devcontainer-features/pre-commit:1\": {}\n  },\n\n  \"postCreateCommand\": \"flutter clean && flutter pub get\",\n\n  \"customizations\": {\n    \"vscode\": {\n      \"extensions\": [\n        \"Dart-Code.dart-code\",\n        \"Dart-Code.flutter\",\n        \"esbenp.prettier-vscode\",\n        \"mkhl.shfmt\",\n        \"redhat.vscode-yaml\"\n      ],\n      \"settings\": {\n        \"[dart]\": {\n          \"editor.formatOnSave\": true,\n          \"editor.codeActionsOnSave\": {\n            \"source.fixAll\": \"explicit\",\n            \"source.organizeImports\": \"explicit\"\n          },\n          \"editor.selectionHighlight\": false,\n          \"editor.tabCompletion\": \"onlySnippets\",\n          \"editor.wordBasedSuggestions\": \"off\"\n        },\n        \"[shellscript]\": {\n          \"editor.formatOnSave\": true,\n          \"editor.codeActionsOnSave\": {\n            \"source.fixAll.shellcheck\": \"explicit\"\n          }\n        },\n        \"shfmt.executableArgs\": [\"-i\", \"2\", \"-s\", \"-bn\", \"-ci\", \"-sr\"],\n        \"shellcheck.customArgs\": [\"-o\", \"all\"],\n        \"shellcheck.enable\": true,\n        \"shellcheck.enableQuickFix\": true,\n        \"shellcheck.exclude\": [],\n        \"[markdown]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[json]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n          \"editor.formatOnSave\": true\n        },\n        \"[jsonc]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n          \"editor.formatOnSave\": true\n        },\n        \"[yaml]\": {\n          \"editor.defaultFormatter\": \"redhat.vscode-yaml\",\n          \"editor.formatOnSave\": true\n        }\n      }\n    }\n  },\n\n  \"containerEnv\": {\n    \"DISPLAY\": \"${localEnv:DISPLAY}\",\n    \"XDG_RUNTIME_DIR\": \"/tmp/vscode-wayland\",\n    \"WAYLAND_DISPLAY\": \"${localEnv:WAYLAND_DISPLAY}\",\n    \"GDK_BACKEND\": \"wayland\",\n    \"QT_QPA_PLATFORM\": \"xcb\",\n    \"QT_X11_NO_MITSHM\": \"1\"\n  },\n\n  \"mounts\": [\n    \"source=${localEnv:XDG_RUNTIME_DIR},target=/tmp/vscode-wayland,type=bind,consistency=cached\",\n    \"source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached\"\n  ]\n}\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"pub\"\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n\njobs:\n  test:\n    name: CI\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install sqlite\n        run: sudo apt-get update && sudo apt-get -y install sqlite3 libsqlite3-dev\n\n      - name: Setup flutter\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: '3.24.5'\n          channel: 'stable'\n          cache: true\n          cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}\n\n      - name: Flutter version\n        run: flutter --version\n\n      - name: Install dependencies\n        run: flutter pub get\n\n      - name: Check licenses\n        run: |\n          dart pub global activate very_good_cli\n          dart pub global run very_good_cli:very_good packages check licenses \\\n            --dependency-type='direct-main,transitive' \\\n            --allowed='MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0,Zlib'\n\n      - name: Analyze code (lint)\n        run: flutter analyze --fatal-infos --fatal-warnings\n\n      - name: Analyze code (format)\n        run: dart format --set-exit-if-changed .\n\n      - name: Run test\n        run: flutter test --coverage\n\n      - name: Check test coverage\n        uses: VeryGoodOpenSource/very_good_coverage@v2\n        with:\n          path: '${{ github.workspace }}/coverage/lcov.info'\n          min_coverage: 60\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+'\n\nenv:\n  PROPERTIES_PATH: \"./android/key.properties\"\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Install sqlite\n        run: sudo apt-get update && sudo apt-get -y install sqlite3 libsqlite3-dev\n\n      - name: Setup java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'temurin'\n          java-version: '21'\n\n      - name: Setup signing environment\n        run: |\n          echo keyPassword=\\${{ secrets.APP_SIGN_KEY_PASSWORD }} > ${{ env.PROPERTIES_PATH }}\n          echo storePassword=\\${{ secrets.APP_SIGN_STORE_PASSWORD }} >> ${{ env.PROPERTIES_PATH }}\n          echo keyAlias=\\${{ secrets.APP_SIGN_KEY_ALIAS }} >> ${{ env.PROPERTIES_PATH }}\n          echo storeFile=key.jks >> ${{ env.PROPERTIES_PATH }}\n          echo \"${{ secrets.APP_SIGN_KEY_JKS }}\" | base64 --decode > android/app/key.jks\n\n      - name: Setup flutter\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: '3.24.5'\n          channel: 'stable'\n          cache: true\n          cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}\n\n      - name: Flutter version\n        run: flutter --version\n\n      - name: Install dependencies\n        run: flutter pub get\n\n      - name: Check licenses\n        run: |\n          dart pub global activate very_good_cli\n          dart pub global run very_good_cli:very_good packages check licenses \\\n            --dependency-type='direct-main,transitive' \\\n            --allowed='MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0,Zlib'\n\n      - name: Analyze code (lint)\n        run: flutter analyze --fatal-infos --fatal-warnings\n\n      - name: Analyze code (format)\n        run: dart format --set-exit-if-changed .\n\n      - name: Run test\n        run: flutter test --coverage\n\n      - name: Check test coverage\n        uses: VeryGoodOpenSource/very_good_coverage@v2\n        with:\n          path: '${{ github.workspace }}/coverage/lcov.info'\n          exclude: '**/*_observer.dart'\n          min_coverage: 60\n\n      - name: Build appbundle\n        run: flutter build appbundle --release\n\n      - name: Build apk\n        run: flutter build apk --release\n\n      - name: Build apk per ABI\n        run: flutter build apk --split-per-abi\n\n      - name: Release\n        uses: ncipollo/release-action@v1\n        with:\n          owner: ${{ github.repository_owner }}\n          body: 'See [CHANGELOG](https://github.com/tmaegel/ntodotxt/blob/main/CHANGELOG.md)'\n          artifacts: >\n            build/app/outputs/bundle/release/app-release.aab,\n            build/app/outputs/flutter-apk/app-release.apk,\n            build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk,\n            build/app/outputs/flutter-apk/app-arm64-v8a-release.apk,\n            build/app/outputs/flutter-apk/app-x86_64-release.apk\n          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.lock\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# Visual Studio Code related\n.classpath\n.project\n.settings/\n\n# Flutter repo-specific\n/bin/cache/\n/bin/internal/bootstrap.bat\n/bin/internal/bootstrap.sh\n/bin/mingit/\n/dev/benchmarks/mega_gallery/\n/dev/bots/.recipe_deps\n/dev/bots/android_tools/\n/dev/devicelab/ABresults*.json\n/dev/docs/doc/\n/dev/docs/flutter.docs.zip\n/dev/docs/lib/\n/dev/docs/pubspec.yaml\n/dev/integration_tests/**/xcuserdata\n/dev/integration_tests/**/Pods\n/packages/flutter/coverage/\nversion\nanalysis_benchmark.json\n\n# packages file containing multi-root paths\n.packages.generated\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n**/generated_plugin_registrant.dart\n.packages\n.pub-preload-cache/\n.pub/\nbuild/\nflutter_*.png\nlinked_*.ds\nunlinked.ds\nunlinked_spec.ds\n\n# Android related\n.android/\n**/android/**/gradle-wrapper.jar\n.gradle/\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n**/android/key.properties\n*.jks\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/.last_build_id\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# macOS\n**/Flutter/ephemeral/\n**/Pods/\n**/macos/Flutter/GeneratedPluginRegistrant.swift\n**/macos/Flutter/ephemeral\n**/xcuserdata/\n\n# Windows\n**/windows/flutter/generated_plugin_registrant.cc\n**/windows/flutter/generated_plugin_registrant.h\n**/windows/flutter/generated_plugins.cmake\n\n# Linux\n**/linux/flutter/generated_plugin_registrant.cc\n**/linux/flutter/generated_plugin_registrant.h\n**/linux/flutter/generated_plugins.cmake\n\n# Coverage\ncoverage/\n\n# Symbols\napp.*.symbols\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n!/dev/ci/**/Gemfile.lock\n!.vscode/settings.json\n\ndocker-compose.yml\n.scannerwork/\n\n# devenv\n.devenv*\ndevenv.local.nix\n\n# direnv\n.direnv\n\n# nix\n!flake.lock\nlocal.properties\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \".flutter\"]\n\tpath = .flutter\n\turl = https://github.com/flutter/flutter\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: \"db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\"\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: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: android\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: ios\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: linux\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: macos\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: web\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n    - platform: windows\n      create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\n      base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a\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": ".pre-commit-config.yaml",
    "content": "exclude: (^android/|^build/|^coverage/|^ios/|^linux/|^macos/|^web/|^windows/)\nfail_fast: true\ndefault_install_hook_types: [pre-commit]\ndefault_stages: [pre-commit]\n\nrepos:\n  - repo: \"https://github.com/pre-commit/pre-commit-hooks\"\n    rev: \"v4.5.0\"\n    hooks:\n      - id: check-added-large-files\n        args: ['--maxkb=1024']\n      - id: check-case-conflict\n      - id: pretty-format-json\n        args: [\"--autofix\", \"--indent=4\", \"--no-sort-keys\"]\n      - id: check-merge-conflict\n      - id: check-symlinks\n      - id: check-yaml\n      - id: check-json\n      - id: check-toml\n      - id: check-xml\n      - id: destroyed-symlinks\n      - id: detect-private-key\n      - id: trailing-whitespace\n      - id: double-quote-string-fixer\n\n  - repo: local\n    hooks:\n      - id: dart format\n        name: dart format\n        language: system\n        entry: dart format --set-exit-if-changed\n        files: \\.dart$\n\n      - id: flutter analyze\n        name: flutter analyze\n        language: system\n        entry: flutter analyze --fatal-infos --fatal-warnings\n        files: \\.dart$\n\n      - id: flutter test\n        name: flutter test\n        language: system\n        entry: flutter test --coverage\n        pass_filenames: false\n\n      - id: test coverage\n        name: test coverage\n        language: system\n        entry: dart run test_cov_console -l\n        pass_filenames: false\n        verbose: true\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"flutter: run (Linux)\",\n      \"type\": \"shell\",\n      \"command\": \"flutter\",\n      \"args\": [\"run\", \"-d\", \"linux\"],\n      \"group\": \"build\",\n      \"problemMatcher\": []\n    },\n    {\n      \"label\": \"flutter: pub get\",\n      \"type\": \"shell\",\n      \"command\": \"flutter\",\n      \"args\": [\"pub\", \"get\"],\n      \"group\": \"build\",\n      \"problemMatcher\": []\n    },\n    {\n      \"label\": \"flutter: clean\",\n      \"type\": \"shell\",\n      \"command\": \"flutter\",\n      \"args\": [\"clean\"],\n      \"group\": \"build\",\n      \"problemMatcher\": []\n    }\n  ]\n}\n"
  },
  {
    "path": ".yamlfmt",
    "content": "formatter:\n  type: basic\n  include_document_start: false\n  retain_line_breaks_single: true\n  trim_trailing_whitespace: true\n  pad_line_comments: 2\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.17.0] - 2026-03-13\n\n### Added\n\n- Option to enable or disable swipe-right and swipe-left actions for list items #107\n- Option to delete filters via swipe-right action\n\n## [0.16.1] - 2026-01-05\n\n### Fixed\n\n- 204 responses indicate no errors during the ping\n\n## [0.16.0] - 2025-09-26\n\n### Added\n\n- Confirmation dialog when deleting a task with a swipe gesture #102\n\n## [0.15.0] - 2025-09-12\n\n### Added\n\n- Mark todos as done/undone via swipe gesture #42 #80\n- Delete todos via swipe gesture\n\n## [0.14.2] - 2025-07-17\n\n### Fixed\n\n- Page rebuild when navigating between filter pages #97\n\n## [0.14.1] - 2025-07-07\n\n### Fixed\n\n- Editing the filter name\n- No longer push main routes to the navigator stack\n\n### Added\n\n- Add FloatingActionButton for easy marking of todos as done/undone #95\n\n## [0.14.0] - 2025-06-22\n\n### Added\n\n- Donation link\n\n### Changed\n\n- Allow to choose completion date #83\n\n### Fixed\n\n- Displays initially an empty search result list if no search term has been entered\n- Don't allow due dates in the past and completion dates in the future\n\n## [0.13.1] - 2025-06-10\n\n### Changed\n\n- Improvement of the database handling\n- No longer prevent landscape mode #88\n\n### Removed\n\n- Deprecated code for backwards compatibility\n\n## [0.13.0] - 2025-05-22\n\n### Changed\n\n- Bump flutter version to 3.24.5\n- Bump version of a bunch of dependencies\n- Sets default localization to 'en' for date picker #90\n\n## [0.12.4] - 2025-03-29\n\n### Fixed\n\n- Recognize whether a file already exists on the server side\n\n### Changed\n\n- Improvement of path handling\n\n## [0.12.3] - 2025-03-15\n\n### Fixed\n\n- Migrate app data from cache directory to data directory #78 #84\n\n## [0.12.2] - 2025-01-06\n\n### Fixed\n\n- Remove duplicate slash in file path #67, #79\n\n### Added\n\n- Add filter/todo save button to app bar\n\n## [0.12.1] - 2024-12-11\n\n### Fixed\n\n- Deselecting priority\n\n## [0.12.0] - 2024-12-02\n\n### Added\n\n- Checkbox to accept untrusted SSL certificates #72\n\n### Changed\n\n- Removes the save button and adds a save/discard dialog instead when leaving the todo/filter page #61\n\n### Fixed\n\n- Tag dialogs don't show all possible tags\n\n## [0.11.0] - 2024-11-01\n\n## Added\n\n- Possibility to edit the due date without resetting it first #52\n- Sort filters alphabetically #50\n- Possibility to show and hide the password in the password field\n\n### Changed\n\n- Auto apply changes in priority, project, context and key-value dialogs #65\n- Auto apply projects and contexts tags if a new todo is created within the filter page #44\n- Context and project tags will no longer change to lower case #64\n\n## [0.10.1] - 2024-09-22\n\n### Changed\n\n- Bump flutter version to 3.19.6\n\n### Fixed\n\n- First word not capitalized #63\n- Allow single character key-value pairs #53\n\n## [0.10.0] - 2024-06-09\n\n## Added\n\n- Possibility to configure the remote path and local/remote filename #56\n\n## Removed\n\n- BREAKING CHANGE: The username is no longer automatically appended to webdav base url (reinitialize your app if needed)\n\n## [0.9.1] - 2024-05-23\n\n### Fixed\n\n- No auto-space is inserted after selecting a word from suggestion #36\n\n## [0.9.0] - 2024-05-03\n\n### Added\n\n- Adds the full range of priorities from A to Z #48\n\n### Fixed\n\n- Removes id from the the todo key values #34\n\n## [0.8.1] - 2024-04-25\n\n### Fixed\n\n- Fixes an issue that the file picker was not opened for android api versions lower than 28 #45\n\n## [0.8.0] - 2024-04-01\n\n### Added\n\n- Custome file name of the local todo file while initialization of the app #35\n\n### Changed\n\n- File name and path can no longer be changed after initializing the app #35\n- Update splash screen\n\n## [0.7.1] - 2024-03-26\n\n### Changed\n\n- Adjusts configuration of textfield suggestions #36\n\n## [0.7.0] - 2024-03-20\n\n### Added\n\n- Add an intro screen #31\n- Highlights filter chip in a different color if filter has updated\n- Tags can now also occur inline of a todo on the list view\n- Long todos are displayed shortened on the list view\n\n### Changed\n\n- Update login screen #31\n- Refactor initial loading and login routines\n- Add hint if no tags are available on tag dialog\n- Improve error handling and the resulting messages\n\n### Fixed\n\n- Trim whitespaces of filter name before updating\n- Fix issue of todo textfield if todo is very long\n- Fix small style issues\n\n## [0.6.2] - 2024-03-13\n\n### Fixed\n\n- Requests folder permission on the initial setup screen #30\n- Base url may also ends with the username #28\n- Updates default filter directly if it has been changed in the settings\n- Sorts todos by description only and completed todos come always at last\n- Resets settings correctly if logout\n\n## [0.6.1] - 2024-03-05\n\n### Added\n\n- Hide keyboard if tap outside of textfield\n\n### Changed\n\n- Bump file_picker to 6.2.0\n- Bump flutter_bloc to 8.1.4\n- Bump go_router to 13.2.0\n- Bump sqflite_common_ffi to 2.3.2+1\n- Bump sqlite3_flutter_libs to 0.5.20\n- Bump url_launcher to 6.2.5\n- Update style of drawer\n- Update style of loading spinner\n\n### Fixed\n\n- Filter todo list on search page correctly\n- Order todos for the different filters/groupings correctly\n- Keep scroll position of todo list if todo was created or edited\n- Solve error while initialization on desktop\n\n## [0.6.0] - 2024-02-28\n\n### Added\n\n- Add new widget tests and refactor existing ones\n\n### Changed\n\n- Disable landscape mode\n- Add a confirmation dialog when the app settings are reset\n- Improve the appearance of the todo list page\n- Make app bar transparent\n- Hide floating action button if keyboard is open\n- Hide floating action button (save) if todo or filter has not be changed\n- Hide floating action button (save) if name todo or filter is empty\n\n### Removed\n\n- Remove the functionality to set the todo completion state by swiping\n\n### Fixed\n\n- Improved error handling on login screen\n- Improved text field behavior when creating or editing todos #27\n- Prevention of + and @ characters at the beginning of the tag when displayed in the tag dialog\n\n## [0.5.1] - 2024-02-21\n\n### Added\n\n- Hide primary floating action button when scrolling down and show 'go to top' button instead\n\n### Changed\n\n- Remove bottom bar\n- Transparent bottom system navigation bar and edge to edge view\n- Small style adjustments of the snackbar and loading indicator\n- Replace app launcher icon\n\n### Fixed\n\n- Dismiss dialogs on back button\n- Resolve some build warnings\n- Resolve some minor theme issues\n\n## [0.5.0] - 2024-02-16\n\n### Added\n\n- Add possibility to customize the local path of the todo.txt file #7\n- Tests the connection to the webdav before login\n\n### Changed\n\n- Improve the appearance of the login screen\n\n### Fixed\n\n- Activate the previous item in the drawer when navigating back\n- Ignore empty lines in todo.txt file\n\n## [0.4.7] - 2024-02-06\n\n### Fixed\n\n- Add missing permission android.permission.INTERNET #20\n\n## [0.4.6] - 2024-02-04\n\n### Fixed\n\n- Pin tag/version of flutter submodule to v3.16.9\n\n## [0.4.5] - 2024-02-03\n\n### Changed\n\n- Bump flutter version to 3.16.9\n- Update some dialogs\n\n### Fixed\n\n- Server port for the webdav connection is optional #12 #17\n- Sometimes the hamburger menu gets lost #18\n- dense attribute is not neccessary for material3 themes\n\n## [0.4.4] - 2024-01-16\n\n### Changed\n\n- Move drawer to appbar (mobile only)\n- Redesign todo and filter detail page/view\n\n### Fixed\n\n- Disable allowBackup in AndroidManifest.xml\n- Some dialogs are scrollable if the keyboad appears\n- Fix regex for hostname validation #8\n\n## [0.4.3] - 2024-01-08\n\n### Changed\n\n- Sign apks\n\n### Fixed\n\n- Add missing `flutter_launcher_icons` dependency\n\n## [0.4.2] - 2024-01-05\n\n### Added\n\n- Add `flutter` as git submodule\n\n## [0.4.1] - 2024-01-05\n\n### Added\n\n- Add metadata (`fastlane`) to get the app ready for deployment in the fdroid store\n\n### Fixed\n\n- Add version code to `pubspec.yaml`\n\n## [0.4.0] - 2024-01-04\n\n### Added\n\n- App icon (made by @colebemis)\n- Confirmation dialog for deleting todo or filter\n\n### Changed\n\n- Update drawer (mobile) style\n- Disable 'Apply' button in dialogs if unnecessary (e.g. empty list)\n- Bump `flutter` to 3.16.5\n- Bump `go_router` to 13.0.1\n- Bump `url_launcher` to 6.2.2\n\n### Removed\n\n- Remove `google_fonts`\n\n## [0.3.0] - 2023-12-22\n\n### Added\n\n- Add functionality to save and manage filters\n- Add database (`sqflite`) and controller to persist data (filter and settings)\n- Add simple loading / splash screen while initialize the app\n\n### Changed\n\n- Save default filter settings in sqlite database instead of shared preferences\n- Theme and UI improvements and some redesign (app bar, dialogs, ...)\n- Replace navigation drawer with bottom sheet (for mobile) and navigation rail (desktop)\n\n### Removed\n\n- Remove dependencie shared_preferences\n- Remove todo selection functionality\n\n### Fixed\n\n- Add error state to FilterState and handle/show errors\n\n## [0.2.0] - 2023-12-11\n\n### Added\n\n- Add swipe (left/right) action to toggle the completion of todo\n\n### Changed\n\n- Minor style adjustments to the theme and layout\n\n### Fixed\n\n- Hide tags (projects, contexts, key values) in tag dialog if already present in todo\n- Toggle filter/order/group by if tapping on the label\n- Notification bars are floating\n\n## [0.1.0] - 2023-12-08\n\n### Added\n\n- Intiial release\n\n[unreleased]: https://github.com/tmaegel/ntodotxt/compare/v0.17.0...HEAD\n[0.16.1]: https://github.com/tmaegel/ntodotxt/compare/v0.16.1...v0.17.0\n[0.16.1]: https://github.com/tmaegel/ntodotxt/compare/v0.16.0...v0.16.1\n[0.16.0]: https://github.com/tmaegel/ntodotxt/compare/v0.15.0...v0.16.0\n[0.15.0]: https://github.com/tmaegel/ntodotxt/compare/v0.14.2...v0.15.0\n[0.14.2]: https://github.com/tmaegel/ntodotxt/compare/v0.14.1...v0.14.2\n[0.14.1]: https://github.com/tmaegel/ntodotxt/compare/v0.14.0...v0.14.1\n[0.14.0]: https://github.com/tmaegel/ntodotxt/compare/v0.13.1...v0.14.0\n[0.13.1]: https://github.com/tmaegel/ntodotxt/compare/v0.13.0...v0.13.1\n[0.13.0]: https://github.com/tmaegel/ntodotxt/compare/v0.12.4...v0.13.0\n[0.12.4]: https://github.com/tmaegel/ntodotxt/compare/v0.12.3...v0.12.4\n[0.12.3]: https://github.com/tmaegel/ntodotxt/compare/v0.12.2...v0.12.3\n[0.12.2]: https://github.com/tmaegel/ntodotxt/compare/v0.12.1...v0.12.2\n[0.12.1]: https://github.com/tmaegel/ntodotxt/compare/v0.12.0...v0.12.1\n[0.12.0]: https://github.com/tmaegel/ntodotxt/compare/v0.11.0...v0.12.0\n[0.11.0]: https://github.com/tmaegel/ntodotxt/compare/v0.10.1...v0.11.0\n[0.10.1]: https://github.com/tmaegel/ntodotxt/compare/v0.10.0...v0.10.1\n[0.10.0]: https://github.com/tmaegel/ntodotxt/compare/v0.9.1...v0.10.0\n[0.9.1]: https://github.com/tmaegel/ntodotxt/compare/v0.9.0...v0.9.1\n[0.9.0]: https://github.com/tmaegel/ntodotxt/compare/v0.8.1...v0.9.0\n[0.8.1]: https://github.com/tmaegel/ntodotxt/compare/v0.8.0...v0.8.1\n[0.8.0]: https://github.com/tmaegel/ntodotxt/compare/v0.7.1...v0.8.0\n[0.7.1]: https://github.com/tmaegel/ntodotxt/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/tmaegel/ntodotxt/compare/v0.6.2...v0.7.0\n[0.6.2]: https://github.com/tmaegel/ntodotxt/compare/v0.6.1...v0.6.2\n[0.6.1]: https://github.com/tmaegel/ntodotxt/compare/v0.6.0...v0.6.1\n[0.6.0]: https://github.com/tmaegel/ntodotxt/compare/v0.5.1...v0.6.0\n[0.5.1]: https://github.com/tmaegel/ntodotxt/compare/v0.5.0...v0.5.1\n[0.5.0]: https://github.com/tmaegel/ntodotxt/compare/v0.4.7...v0.5.0\n[0.4.7]: https://github.com/tmaegel/ntodotxt/compare/v0.4.6...v0.4.7\n[0.4.6]: https://github.com/tmaegel/ntodotxt/compare/v0.4.5...v0.4.6\n[0.4.5]: https://github.com/tmaegel/ntodotxt/compare/v0.4.4...v0.4.5\n[0.4.4]: https://github.com/tmaegel/ntodotxt/compare/v0.4.3...v0.4.4\n[0.4.3]: https://github.com/tmaegel/ntodotxt/compare/v0.4.2...v0.4.3\n[0.4.2]: https://github.com/tmaegel/ntodotxt/compare/v0.4.1...v0.4.2\n[0.4.1]: https://github.com/tmaegel/ntodotxt/compare/v0.4.0...v0.4.1\n[0.4.0]: https://github.com/tmaegel/ntodotxt/compare/v0.3.0...v0.4.0\n[0.3.0]: https://github.com/tmaegel/ntodotxt/compare/v0.2.0...v0.3.0\n[0.2.0]: https://github.com/tmaegel/ntodotxt/compare/v0.1.0...v0.2.0\n[0.1.0]: https://github.com/tmaegel/ntodotxt/releases/tag/v0.1.0\n"
  },
  {
    "path": "Caddyfile",
    "content": "{\n  debug\n}\n\nlocalhost {\n  uri strip_prefix /nc\n  reverse_proxy nextcloud:80\n}\n\n10.0.2.2 {\n  tls internal\n  uri strip_prefix /nc\n  reverse_proxy nextcloud:80\n}\n"
  },
  {
    "path": "Dockerfile_fdroid",
    "content": "FROM registry.gitlab.com/fdroid/docker-executable-fdroidserver:master\n\nRUN apt-get update && apt-get install -y \\\n  openjdk-17-jdk-headless \\\n  && rm -rf /var/lib/apt/lists/* \\\n  && update-java-alternatives -a\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Toni Mägel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: screenshots\n\nAPP_ID = \"de.tnmgl.ntodotxt\"\nFDROID_REPO = \"${HOME}/Downloads/nosync/fdroiddata\"\n\nsonarqube:\n\tdocker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 127.0.0.1:9000:9000 sonarqube:10.4-community\n\nscan:\n\tdocker run \\\n    --rm --name sonar-scanner-cli \\\n    -e SONAR_HOST_URL=\"http://sonarqube:9000\" \\\n    -e SONAR_SCANNER_OPTS=\"-Dsonar.projectKey=ntodotxt\" \\\n    -e SONAR_TOKEN=\"token\" \\\n    -v \"$(shell pwd):/usr/src\" \\\n\t\t--link sonarqube \\\n    sonarsource/sonar-scanner-cli\n\nlicenses:\n\tflutter pub run flutter_oss_licenses:generate.dart\n\nicon:\n\tflutter pub run flutter_launcher_icons\n\nintegration_env_configure:\n\tdocker exec -it -u www-data nextcloud_local php occ config:system:set trusted_domains 2 --value=10.0.2.2\n\nscreenshots:\n\tflutter emulators --launch \"Pixel_7_API_34_extension_level_7_x86_64\" && sleep 10\n\tflutter drive \\\n\t\t--driver=test_driver/screenshot_integration_test.dart \\\n\t\t--target=integration_test/screenshot_integration_test.dart || true\n\tadb emu kill\n\npreview_screenshots:\n\tflutter emulators --launch \"Pixel_7_API_34_extension_level_7_x86_64\" && sleep 10\n\tflutter drive \\\n\t\t--driver=test_driver/screenshot_integration_test.dart \\\n\t\t--target=integration_test/preview_app_integration_test.dart || true\n\tadb emu kill\n\nfdroid_lint:\n\tcd $(FDROID_REPO) && docker run \\\n\t\t--rm \\\n\t\t--name fdroid \\\n\t\t-v \"${HOME}/.android-sdk/\":/opt/android-sdk \\\n\t\t-v $(FDROID_REPO):/repo \\\n\t\t-e ANDROID_HOME:/opt/android-sdk \\\n\t\tregistry.gitlab.com/fdroid/docker-executable-fdroidserver:master lint -v $(APP_ID)\n\nfdroid_signature:\n\tapksigner verify --print-certs app.apk | grep SHA-256\n\tcd $(FDROID_REPO) && docker run \\\n\t\t--rm \\\n\t\t--name fdroid \\\n\t\t-v \"${HOME}/.android-sdk/\":/opt/android-sdk \\\n\t\t-v $(FDROID_REPO):/repo \\\n\t\t-e ANDROID_HOME:/opt/android-sdk \\\n\t\tregistry.gitlab.com/fdroid/docker-executable-fdroidserver:master signatures unsigned/app.apk\n\nfdroid_build:\n\tdocker build -t fdroidserver -f Dockerfile_fdroid .\n\nfdroid_run:\n\tcd $(FDROID_REPO) && docker run \\\n\t\t--rm \\\n\t\t--name fdroid \\\n\t\t-v \"${HOME}/.android-sdk/\":/opt/android-sdk \\\n\t\t-v $(FDROID_REPO):/repo \\\n\t\t-e ANDROID_HOME:/opt/android-sdk \\\n\t\tfdroidserver build -v -l $(APP_ID)\n"
  },
  {
    "path": "README.md",
    "content": "# ntodotxt\n\n[![CI](https://github.com/tmaegel/ntodotxt/actions/workflows/ci.yaml/badge.svg)](https://github.com/tmaegel/ntodotxt/actions/workflows/ci.yaml)\n[![Release](https://img.shields.io/github/v/release/tmaegel/ntodotxt)](https://github.com/tmaegel/ntodotxt/releases)\n[![F-Droid](https://img.shields.io/f-droid/v/de.tnmgl.ntodotxt.svg?logo=F-Droid)](https://f-droid.org/packages/de.tnmgl.ntodotxt)\n[![License](https://img.shields.io/badge/License-MIT-yellow)](https://opensource.org/licenses/MIT)\n[![Flutter](https://img.shields.io/badge/_Flutter_-3.24.5-grey.svg?&logo=Flutter&logoColor=white&labelColor=blue)](https://github.com/flutter/flutter)\n\nWith `ntodotxt` you can manage your todos in a [todo.txt](https://github.com/todotxt/todo.txt) file (i.e. all information\nis stored in a single file). You can save your todos locally on your device and/or synchronize the todo.txt file via webdav - for\nexample with a self-hosted nextcloud instance.\n\nThis application is under active development and will continue to be modified and improved over time.\n\n## Screenshots\n\n<a href=\"https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/screenshots/preview/1.png\"><img src=\"screenshots/preview/1.png\" width=\"19.2%\"/></a>\n<a href=\"https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/screenshots/preview/2.png\"><img src=\"screenshots/preview/2.png\" width=\"19.2%\"/></a>\n<a href=\"https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/screenshots/preview/3.png\"><img src=\"screenshots/preview/3.png\" width=\"19.2%\"/></a>\n<a href=\"https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/screenshots/preview/4.png\"><img src=\"screenshots/preview/4.png\" width=\"19.2%\"/></a>\n<a href=\"https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/screenshots/preview/5.png\"><img src=\"screenshots/preview/5.png\" width=\"19.2%\"/></a>\n\n## Downloads\n\n[<img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\" alt=\"Get it on F-Droid\" height=\"80\">](https://f-droid.org/packages/de.tnmgl.ntodotxt/)\n\n## Features\n\n### v1.0\n\n- [x] Manage todos in [todo.txt](https://github.com/todotxt/todo.txt) format\n- [x] Manage todos locally and/or synchronize todos via webdav with a server of your choice\n- [x] Custom path and filename of todo files (local and remote)\n- [x] Search todos\n- [x] Create custom views of todos via filters\n- [ ] Sort (ascending/descending) todos by criteria such as priority, creation date or due date\n- [ ] Android widget\n- [ ] Import/Export existing todos from/to file\n- [ ] Import/Export filters and other settings\n- [ ] Language localization (e.g. english, german)\n- [ ] [Recurring](https://c306.net/t/topydo-docs/#Recurrence) tasks\n- [ ] Archiving of completed todos (done.txt)\n- [ ] ...\n\n### Low priority\n\n- [ ] Build and publish to Google Play (Android)\n- [ ] Build and publish as `flatpak` to [flathub](https://flathub.org/) (Linux)\n- [ ] Build and publish as `snap` to [snapcraft](https://snapcraft.io/) (Linux)\n- [ ] Build and publish to Microsoft Store (Windows)\n\n## Build\n\n### General\n\n[Flutter SDK](https://docs.flutter.dev/get-started/install) is required to build this project.\n\n### Building on Linux\n\n1. First you need to get the source code of `ntodotxt`.\n\n```bash\ngit clone https://github.com/tmaegel/ntodotxt\n```\n\n2. Installing the dependencies for [sqflite](https://pub.dev/packages/sqflite_common_ffi#linux) and [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage#configure-linux-version).\n\n```bash\ndnf install sqlite-devel libsecret-devel1\n```\n\n3. Open project via [Android Studio](https://developer.android.com/studio).\n\n4. Click the `Run` button and it will be built and run automatically.\n\n5. Or you can build and run from command line.\n\n```bash\nflutter pub get\nflutter run\n# or\nflutter build\n```\n\nIf an error occurs during the build process, please follow these [steps](https://docs.flutter.dev/get-started/install/linux/desktop#development-tools).\n\n## Sponsorship\n\n`ntodotxt` is a free open source software that benefits from the open source community and every user can enjoy it's full functionality for free, so if you appreciate my current work, you can buy me a offee.\n\n<a href='https://ko-fi.com/N4N41GAU03' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi5.png?v=6' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>\n\nThanks for all the love and support.\n\n## Alternatives\n\nThere are a bunch of other note taking apps with the WebDAV support. See them in [awesome-webdav](https://github.com/WebDAVDevs/awesome-webdav/blob/main/readme.md#android-other-apps) repository.\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\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 https://dart.dev/lints.\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    prefer_single_quotes: true\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/to/reference-keystore\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "plugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n    // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.\n    id \"dev.flutter.flutter-gradle-plugin\"\n}\n\ndef keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\nandroid {\n    namespace = \"de.tnmgl.ntodotxt\"\n    compileSdk = 35\n    ndkVersion = \"25.1.8937393\"\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_21\n    }\n\n    defaultConfig {\n        applicationId = \"de.tnmgl.ntodotxt\"\n        minSdk = flutter.minSdkVersion\n        targetSdk = flutter.targetSdkVersion\n        versionCode = flutter.versionCode\n        versionName = flutter.versionName\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        }\n    }\n\n    // Removes DependencyInfoBlock for F-Droid.\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 * 10 + abiVersionCode\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n    <application\n        android:label=\"ntodotxt\"\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:allowBackup=\"false\"\n        android:requestLegacyExternalStorage=\"true\"\n        android:enableOnBackInvokedCallback=\"true\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:taskAffinity=\"\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|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    </application>\n    <!-- Required to query activities that can process text, see:\n         https://developer.android.com/training/package-visibility 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>\n"
  },
  {
    "path": "android/app/src/main/kotlin/de/tnmgl/ntodotxt/MainActivity.kt",
    "content": "package de.tnmgl.ntodotxt\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n</manifest>\n"
  },
  {
    "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}\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.4-all.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError\nandroid.useAndroidX=true\nandroid.enableJetifier=true\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\n    includeBuild(\"$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.3.2\" apply false\n    id \"org.jetbrains.kotlin.android\" version \"2.0.20\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  nextcloud:\n    image: nextcloud:27\n    container_name: nextcloud\n    expose:\n      - 80\n    environment:\n      NEXTCLOUD_TRUSTED_DOMAINS: \"127.0.0.1,10.0.2.2\"\n      OVERWRITEHOST: \"localhost:8443\"\n      OVERWRITEPROTOCOL: \"https\"\n    volumes:\n      - nextcloud:/var/www/html\n    networks:\n      - caddy\n\n  caddy:\n    image: caddy:latest\n    container_name: reverse_proxy\n    ports:\n      - \"127.0.0.1:8080:80\"\n      - \"127.0.0.1:8443:443\"\n      - \"127.0.0.1:8443:443/udp\"\n    volumes:\n      - ./Caddyfile:/etc/caddy/Caddyfile:z\n    networks:\n      - caddy\n\nvolumes:\n  nextcloud:\n  caddy:\n\nnetworks:\n  caddy:\n"
  },
  {
    "path": "emulatorctl",
    "content": "#!/usr/bin/env bash\n\nfunction show_usage() {\n  echo \"Usage:\n    $(basename \"$\") [options]\"\n  echo \"\"\n  echo \"Options:\n    -l        List emulators / avds\n    -c [name] Create emulator / avd\n    -d        Delete emulator / avd\n    -r        Run emulator / avd\n    -e        Exit emulator / avd\n    -v [api]  Anroid API\n    -h        Display this message\"\n}\n\nif [[ $# -eq 0 ]]; then\n  show_usage\n  exit 1\nfi\n\nACTION=0\nANDROID_API=36\nEMULATOR_NAME=\"\"\n\nwhile getopts \":hldrec:v:\" arg; do\n  case \"${arg}\" in\n    l)\n      avdmanager list avd\n      exit 0\n      ;;\n    d)\n      avdmanager delete avd --name \"$(avdmanager list avd -c | fzf)\"\n      exit 0\n      ;;\n    r)\n      echo \"Starting emulator ...\"\n      flutter emulators --launch \"$(avdmanager list avd -c | fzf)\"\n      exit 0\n      ;;\n    e)\n      echo \"Exiting emulator ...\"\n      adb emu kill\n      exit 0\n      ;;\n    c)\n      ACTION=\"create\"\n      EMULATOR_NAME=\"${OPTARG}\"\n      ;;\n    v)\n      ANDROID_API=\"${OPTARG}\"\n      ;;\n    h | *)\n      show_usage\n      exit 0\n      ;;\n  esac\ndone\n\nif [[ ${ACTION} == \"create\" ]]; then\n  avdmanager create avd --name \"${EMULATOR_NAME}-${ANDROID_API}\" --package \"system-images;android-${ANDROID_API};google_apis;x86_64\"\nfi\n\nexit 0\n"
  },
  {
    "path": "fonts/LICENSE",
    "content": "Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded,\r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "integration_test/login/login_integration_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:ntodotxt/intro/page/intro_page.dart';\nimport 'package:ntodotxt/login/page/login_page.dart';\nimport 'package:ntodotxt/main.dart';\nimport 'package:path_provider/path_provider.dart';\n\nvoid main() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n  SystemChrome.setPreferredOrientations(\n    [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],\n  );\n\n  final String appDataDir =\n      '${(await getApplicationDocumentsDirectory()).path}/';\n\n  group('login', () {\n    group('initial', () {\n      testWidgets('screen is visible', (tester) async {\n        await tester.pumpWidget(\n          App(appDataDir: appDataDir),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n        expect(find.byType(IntroPage), findsOneWidget);\n      });\n    });\n\n    group('local', () {\n      testWidgets('default local path', (tester) async {\n        await tester.pumpWidget(\n          App(appDataDir: appDataDir),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n        expect(find.byType(IntroPage), findsOneWidget);\n        await tester.tap(find.byTooltip('Next page'));\n        await tester.pumpAndSettle();\n        await tester.tap(find.text('Use local mode'));\n        await tester.pumpAndSettle();\n        expect(find.byType(LocalLoginView), findsOneWidget);\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                widget.title is Text &&\n                (widget.title as Text).data == 'Local path' &&\n                widget.subtitle != null &&\n                (widget.subtitle as Text).data == appDataDir,\n          ),\n          findsOneWidget,\n        );\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.byType(App), findsOneWidget);\n\n        await tester.tap(find.byTooltip('Open drawer'));\n        await tester.pumpAndSettle();\n        await tester.drag(\n            find.byType(DraggableScrollableSheet), const Offset(0, -500));\n        await tester.pumpAndSettle();\n        await tester.tap(find.text('Settings'));\n        await tester.pumpAndSettle();\n        await tester.scrollUntilVisible(find.text('Reinitialization'), 500);\n        await tester.tap(find.text('Reinitialization'));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(AlertDialog),\n            matching: find.text('Reninitialize'),\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n        expect(find.byType(IntroPage), findsOneWidget);\n      });\n    });\n\n    group('webdav', () {\n      testWidgets('default local path', (tester) async {\n        await tester.pumpWidget(\n          App(appDataDir: appDataDir),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n        expect(find.byType(IntroPage), findsOneWidget);\n        await tester.tap(find.byTooltip('Next page'));\n        await tester.pumpAndSettle();\n        await tester.tap(find.byTooltip('Next page'));\n        await tester.pumpAndSettle();\n        await tester.tap(find.text('Use webdav mode'));\n        await tester.pumpAndSettle();\n        expect(find.byType(WebDAVLoginView), findsOneWidget);\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                widget.title is Text &&\n                (widget.title as Text).data == 'Local path' &&\n                widget.subtitle != null &&\n                (widget.subtitle as Text).data == appDataDir,\n          ),\n          findsOneWidget,\n        );\n\n        await tester.enterText(\n          find.ancestor(\n            of: find.text('Server'),\n            matching: find.byType(TextFormField),\n          ),\n          'https://10.0.2.2:8443',\n        );\n        await tester.tap(find.byType(Checkbox));\n        await tester.enterText(\n          find.ancestor(\n            of: find.text('Path'),\n            matching: find.byType(TextFormField),\n          ),\n          '/remote.php/dav/files/test',\n        );\n        await tester.enterText(\n          find.ancestor(\n            of: find.text('Username'),\n            matching: find.byType(TextFormField),\n          ),\n          'test',\n        );\n        await tester.enterText(\n          find.ancestor(\n            of: find.text('Password'),\n            matching: find.byType(TextFormField),\n          ),\n          'test',\n        );\n        await tester.pumpAndSettle();\n        await tester.tap(find.byType(AppBar));\n        await tester.pumpAndSettle();\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.byType(App), findsOneWidget);\n\n        await tester.tap(find.byTooltip('Open drawer'));\n        await tester.pumpAndSettle();\n        await tester.drag(\n            find.byType(DraggableScrollableSheet), const Offset(0, -500));\n        await tester.pumpAndSettle();\n        await tester.tap(find.text('Settings'));\n        await tester.pumpAndSettle();\n\n        await tester.scrollUntilVisible(find.text('Reinitialization'), 500);\n        await tester.tap(find.text('Reinitialization'));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(AlertDialog),\n            matching: find.text('Reninitialize'),\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n        expect(find.byType(IntroPage), findsOneWidget);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "integration_test/preview_app_integration_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nclass AppPreview extends StatelessWidget {\n  final String image;\n  final String message;\n  final Color foregroundColor = Colors.white;\n  final Color backgroundColor = Colors.lightBlue[100]!;\n  final Color deviceFrameColor = Colors.blueGrey;\n\n  AppPreview({\n    required this.image,\n    required this.message,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      home: Scaffold(\n        backgroundColor: backgroundColor,\n        body: Container(\n          padding: const EdgeInsets.only(bottom: 24.0),\n          child: Column(\n            children: [\n              Flexible(\n                flex: 1,\n                child: Center(\n                  child: Text(\n                    message,\n                    style: Theme.of(context).textTheme.titleLarge,\n                  ),\n                ),\n              ),\n              Flexible(\n                flex: 6,\n                child: Stack(\n                  alignment: Alignment.topCenter,\n                  children: [\n                    Container(\n                      decoration: BoxDecoration(\n                        color: deviceFrameColor,\n                        border: Border.all(\n                          width: 12,\n                          color: deviceFrameColor,\n                        ),\n                        borderRadius: BorderRadius.circular(34),\n                      ),\n                      child: Stack(\n                        alignment: Alignment.topLeft,\n                        children: [\n                          Stack(\n                            alignment: Alignment.topRight,\n                            children: [\n                              ClipRRect(\n                                borderRadius: BorderRadius.circular(24),\n                                child: Image.network(\n                                  image,\n                                  fit: BoxFit.cover,\n                                ),\n                              ),\n                              Padding(\n                                padding: const EdgeInsets.symmetric(\n                                    horizontal: 12.0, vertical: 8.0),\n                                child: Row(\n                                  mainAxisSize: MainAxisSize.min,\n                                  crossAxisAlignment: CrossAxisAlignment.center,\n                                  children: [\n                                    Icon(\n                                      Icons.signal_wifi_4_bar,\n                                      color: foregroundColor,\n                                      size: Theme.of(context)\n                                          .textTheme\n                                          .bodySmall\n                                          ?.fontSize,\n                                    ),\n                                    Icon(\n                                      Icons.signal_cellular_4_bar,\n                                      color: foregroundColor,\n                                      size: Theme.of(context)\n                                          .textTheme\n                                          .bodySmall\n                                          ?.fontSize,\n                                    ),\n                                    const SizedBox(width: 2.0),\n                                    Icon(\n                                      Icons.battery_full,\n                                      color: foregroundColor,\n                                      size: Theme.of(context)\n                                          .textTheme\n                                          .bodySmall\n                                          ?.fontSize,\n                                    ),\n                                  ],\n                                ),\n                              ),\n                            ],\n                          ),\n                          Padding(\n                            padding: const EdgeInsets.symmetric(\n                                horizontal: 12.0, vertical: 8.0),\n                            child: Text(\n                              '12:00',\n                              style: TextStyle(\n                                fontSize: Theme.of(context)\n                                    .textTheme\n                                    .bodySmall\n                                    ?.fontSize,\n                                color: foregroundColor,\n                              ),\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() async {\n  const String repoUrl =\n      'https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/';\n  final IntegrationTestWidgetsFlutterBinding binding =\n      IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  WidgetsFlutterBinding.ensureInitialized();\n  // Hide android status bar.\n  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);\n\n  group('dark mode', () {\n    group('create app store preview screenshots', () {\n      testWidgets('of todo list (default)', (tester) async {\n        await tester.pumpWidget(\n          AppPreview(\n            message: 'Compact overview of all todos',\n            image: '$repoUrl/screenshots/phone/1.png',\n          ),\n        );\n        // Ensure the image is loaded/displayed.\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('preview/1');\n      });\n      testWidgets('of todo list (with open drawer)', (tester) async {\n        await tester.pumpWidget(\n          AppPreview(\n            message: 'Custom filters',\n            image: '$repoUrl/screenshots/phone/2.png',\n          ),\n        );\n        // Ensure the image is loaded/displayed.\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('preview/2');\n      });\n      testWidgets('of todo edit page', (tester) async {\n        await tester.pumpWidget(\n          AppPreview(\n            message: 'Edit and create todos',\n            image: '$repoUrl/screenshots/phone/3.png',\n          ),\n        );\n        // Ensure the image is loaded/displayed.\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('preview/3');\n      });\n      testWidgets('of filter list (default)', (tester) async {\n        await tester.pumpWidget(\n          AppPreview(\n            message: 'Compact overview of all filters',\n            image: '$repoUrl/screenshots/phone/4.png',\n          ),\n        );\n        // Ensure the image is loaded/displayed.\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('preview/4');\n      });\n      testWidgets('of filter edit page', (tester) async {\n        await tester.pumpWidget(\n          AppPreview(\n            message: 'Edit and create filters',\n            image: '$repoUrl/screenshots/phone/5.png',\n          ),\n        );\n        // Ensure the image is loaded/displayed.\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('preview/5');\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "integration_test/screenshot_integration_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart'\n    show FilterController;\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show Filter, ListFilter, ListGroup, ListOrder;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/login/state/login_state.dart'\n    show LoginLocal, LoginState, LoginWebDAV;\nimport 'package:ntodotxt/main.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo;\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\n// https://developer.android.com/studio/run/emulator-networking#networkaddresses\n// Special alias to your host loopback interface (127.0.0.1 on your development machine)\nconst String server = 'https://10.0.2.2:8443';\nconst String path = '/remote.php/dav/files';\nconst String username = 'test';\nconst String password = 'test';\n\nclass FakeController extends Fake implements FilterController {\n  List<Filter> items = [\n    const Filter(\n      id: 1,\n      name: 'Agenda',\n      order: ListOrder.ascending,\n      filter: ListFilter.incompletedOnly,\n      group: ListGroup.upcoming,\n    ),\n    const Filter(\n      id: 2,\n      name: 'Highly prioritized',\n      order: ListOrder.ascending,\n      filter: ListFilter.incompletedOnly,\n      group: ListGroup.project,\n      priorities: {Priority.A},\n    ),\n    const Filter(\n      id: 3,\n      name: 'Projectideas',\n      order: ListOrder.ascending,\n      filter: ListFilter.incompletedOnly,\n      group: ListGroup.none,\n      projects: {'projectideas'},\n    ),\n    const Filter(\n      id: 4,\n      name: 'Completed only',\n      order: ListOrder.ascending,\n      filter: ListFilter.completedOnly,\n      group: ListGroup.none,\n    ),\n  ];\n\n  @override\n  Future<List<Filter>> list() async {\n    return Future.value(items);\n  }\n}\n\nclass AppTester extends StatelessWidget {\n  final DatabaseController dbController =\n      const DatabaseController(inMemoryDatabasePath);\n  final ThemeMode? themeMode;\n  final String appCacheDir;\n\n  const AppTester({\n    this.themeMode,\n    required this.appCacheDir,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FakeController(),\n          ),\n        ),\n      ],\n      child: MultiBlocProvider(\n        providers: [\n          BlocProvider<LoginCubit>(\n            create: (BuildContext context) => LoginCubit(\n              state: const LoginWebDAV(\n                server: server,\n                path: path,\n                username: username,\n                password: password,\n                acceptUntrustedCert: true,\n              ),\n            ),\n          ),\n          BlocProvider<TodoFileCubit>(\n            create: (BuildContext context) => TodoFileCubit(\n              repository: context.read<SettingRepository>(),\n              localPath: appCacheDir,\n            )..load(),\n          ),\n          BlocProvider<DrawerCubit>(\n            create: (BuildContext context) => DrawerCubit(),\n          ),\n          // Default filter\n          BlocProvider<FilterCubit>(\n            create: (BuildContext context) => FilterCubit(\n              settingRepository: context.read<SettingRepository>(),\n              filterRepository: context.read<FilterRepository>(),\n            )..load(),\n          ),\n          BlocProvider<FilterListBloc>(\n            create: (BuildContext context) => FilterListBloc(\n              repository: context.read<FilterRepository>(),\n            )\n              ..add(const FilterListSubscriped())\n              ..add(const FilterListSynchronizationRequested()),\n          ),\n        ],\n        child: Builder(\n          builder: (BuildContext context) {\n            return BlocBuilder<LoginCubit, LoginState>(\n              builder: (BuildContext context, LoginState state) {\n                if (state is LoginLocal || state is LoginWebDAV) {\n                  return CoreApp(loginState: state);\n                } else {\n                  return const InitialApp();\n                }\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() async {\n  final IntegrationTestWidgetsFlutterBinding binding =\n      IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n\n  final DateTime today = DateTime.now();\n  final List<Todo> todoList = [\n    Todo(\n      creationDate: today.subtract(const Duration(days: 7)),\n      priority: Priority.A,\n      description:\n          'Automate the generation of +app screenshots +learnflutter @development @automation @productivity due:${Todo.date2Str(today.add(const Duration(days: 3)))!}',\n    ),\n    Todo(\n      creationDate: today.subtract(const Duration(days: 14)),\n      priority: Priority.B,\n      description:\n          'Publish this +app +learnflutter @development due:${Todo.date2Str(today.add(const Duration(days: 7)))!}',\n    ),\n    Todo(\n      creationDate: today.subtract(const Duration(days: 2)),\n      description:\n          'Increase test +coverage for this +app +learnflutter @development @testing @productivity',\n    ),\n    Todo(\n      creationDate: today.subtract(const Duration(days: 2)),\n      completion: true,\n      completionDate: today.subtract(const Duration(days: 1)),\n      description:\n          'Write some tests for this +app +learnflutter @development @testing @productivity',\n    ),\n    Todo(\n      creationDate: today.subtract(const Duration(days: 21)),\n      priority: Priority.C,\n      description:\n          'Setup a good project management tool @development @productivity',\n    )\n  ];\n\n  setUp(() async {\n    // Setup todos.\n    WebDAVClient client = WebDAVClient(\n      server: server,\n      path: path,\n      username: username,\n      password: password,\n    );\n    try {\n      await client.upload(\n          content: todoList.join(Platform.lineTerminator),\n          filename: 'todo.txt');\n    } catch (e) {\n      fail('An exception was thrown: $e');\n    }\n  });\n\n  group('dark mode', () {\n    group('take screenshots', () {\n      testWidgets('of todo list (default)', (tester) async {\n        await tester.pumpWidget(\n          AppTester(\n            themeMode: ThemeMode.dark,\n            appCacheDir: (await getApplicationCacheDirectory()).path,\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('phone/1');\n      });\n      testWidgets('of todo list (with open drawer)', (tester) async {\n        await tester.pumpWidget(\n          AppTester(\n            themeMode: ThemeMode.dark,\n            appCacheDir: (await getApplicationCacheDirectory()).path,\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await tester.tap(find.byTooltip('Open drawer'));\n        await tester.pumpAndSettle();\n\n        await tester.drag(\n            find.byType(DraggableScrollableSheet), const Offset(0, -500));\n        await tester.pumpAndSettle();\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('phone/2');\n      });\n      testWidgets('of todo edit page', (tester) async {\n        await tester.pumpWidget(\n          AppTester(\n            themeMode: ThemeMode.dark,\n            appCacheDir: (await getApplicationCacheDirectory()).path,\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await tester.tap(find.text(\n          'Publish this +app +learnflutter @development',\n          findRichText: true,\n        ));\n        await tester.pumpAndSettle();\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('phone/3');\n      });\n      testWidgets('of filter list (default)', (tester) async {\n        await tester.pumpWidget(\n          AppTester(\n            themeMode: ThemeMode.dark,\n            appCacheDir: (await getApplicationCacheDirectory()).path,\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await tester.tap(find.byTooltip('Open drawer'));\n        await tester.pumpAndSettle();\n\n        await tester.drag(\n            find.byType(DraggableScrollableSheet), const Offset(0, -500));\n        await tester.pumpAndSettle();\n\n        await tester.tap(find.text('Filters'));\n        await tester.pumpAndSettle();\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('phone/4');\n      });\n      testWidgets('of filter edit page', (tester) async {\n        await tester.pumpWidget(\n          AppTester(\n            themeMode: ThemeMode.dark,\n            appCacheDir: (await getApplicationCacheDirectory()).path,\n          ),\n        );\n        await tester.pumpAndSettle(const Duration(milliseconds: 5000));\n\n        await tester.tap(find.byTooltip('Open drawer'));\n        await tester.pumpAndSettle();\n\n        await tester.drag(\n            find.byType(DraggableScrollableSheet), const Offset(0, -500));\n        await tester.pumpAndSettle();\n\n        await tester.tap(find.text('Filters'));\n        await tester.pumpAndSettle();\n\n        await tester.tap(find.text('Projectideas'));\n        await tester.pumpAndSettle();\n\n        await binding.convertFlutterSurfaceToImage();\n        await tester.pumpAndSettle();\n        await binding.takeScreenshot('phone/5');\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "integration_test/webdav/client/webdav_client_test.dart",
    "content": "import 'dart:io' show Platform;\nimport 'dart:math';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\nimport 'package:webdav_client/webdav_client.dart';\n\nconst String scheme = 'https';\nconst int port = 8443;\nfinal String host = Platform.isAndroid ? '10.0.2.2' : 'localhost';\n\nWebDAVClient createWebDAVClient({\n  String? server,\n  String? path,\n  String? username,\n  String? password,\n}) {\n  return WebDAVClient(\n    server: server ?? '$scheme://$host:$port',\n    path: path ?? '/remote.php/dav/files/test',\n    username: username ?? 'test',\n    password: password ?? 'test',\n    acceptUntrustedCert: true,\n  );\n}\n\nString randomString({int len = 8}) {\n  final Random r = Random();\n  const chars =\n      'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';\n  return List.generate(len, (index) => chars[r.nextInt(chars.length)]).join();\n}\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('WebDAVClient', () {\n    group('ping()', () {\n      test('correct connection', () async {\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.ping();\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('wrong host', () async {\n        final WebDAVClient client =\n            createWebDAVClient(server: '$scheme://webdav:$port');\n        expectLater(\n          () async => await client.ping(),\n          throwsA(\n            isA<WebDAVClientException>(),\n          ),\n        );\n      });\n      test('wrong port', () async {\n        final WebDAVClient client =\n            createWebDAVClient(server: '$scheme://$host:9999');\n        expectLater(\n          () async => await client.ping(),\n          throwsA(\n            isA<WebDAVClientException>(),\n          ),\n        );\n      });\n    });\n\n    group('listFiles()', () {\n      test('list', () async {\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.listFiles(path: '/');\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n    });\n\n    group('file create() & fileExists()', () {\n      test('file does exist (1)', () async {\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.create(filename);\n          expectLater(\n            await client.fileExists(filename: filename),\n            true,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file does exist (2)', () async {\n        final String directory = randomString();\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.mkdir(directory: directory);\n          await client.create('$directory/$filename');\n          expectLater(\n            await client.fileExists(filename: '$directory/$filename'),\n            true,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file doesn\\'t exist (1)', () async {\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          expectLater(\n            await client.fileExists(filename: 'abc.xyz'),\n            false,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file doesn\\'t exist (2)', () async {\n        final String directory = randomString();\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.mkdir(directory: directory);\n          expectLater(\n            await client.fileExists(filename: '/$directory/abc.xyz'),\n            false,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('directory/file doesn\\'t exist in this path', () async {\n        final String directory = randomString();\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.mkdir(directory: directory);\n          await client.create(filename);\n          expectLater(\n            await client.fileExists(filename: '$directory/$filename'),\n            false,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n    });\n\n    group('upload()', () {\n      test('file upload', () async {\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.upload(content: 'abc', filename: filename);\n          expectLater(await client.fileExists(filename: filename), true);\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file upload within already existing directory', () async {\n        final String directory = randomString();\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.mkdir(directory: directory);\n          await client.upload(content: 'abc', filename: '$directory/$filename');\n          expectLater(\n            await client.fileExists(filename: '$directory/$filename'),\n            true,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file upload within non existing directory', () async {\n        final String directory = randomString();\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.upload(content: 'abc', filename: '$directory/$filename');\n          expectLater(\n            await client.fileExists(filename: '$directory/$filename'),\n            true,\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('exception', () async {\n        final WebDAVClient client = createWebDAVClient(password: 'wrong');\n        expectLater(\n          () async => await client.upload(content: 'abc', filename: 'todo.txt'),\n          throwsA(\n            isA<WebDAVClientException>(),\n          ),\n        );\n      });\n    });\n\n    group('download()', () {\n      test('file download', () async {\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.upload(content: 'abc', filename: filename);\n          expectLater(await client.download(filename: filename), 'abc');\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('file download within directory', () async {\n        final String directory = randomString();\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.upload(content: 'abc', filename: '$directory/$filename');\n          expectLater(\n            await client.download(filename: '$directory/$filename'),\n            'abc',\n          );\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('exception', () async {\n        final WebDAVClient client = createWebDAVClient(password: 'wrong');\n        expectLater(\n          () async => await client.download(filename: 'todo.txt'),\n          throwsA(\n            isA<WebDAVClientException>(),\n          ),\n        );\n      });\n    });\n\n    group('getFile()', () {\n      test('valid', () async {\n        final String filename = '${randomString()}.txt';\n        final WebDAVClient client = createWebDAVClient();\n        try {\n          await client.upload(content: 'abc', filename: filename);\n          File f = await client.getFile(filename: filename);\n          expect(f.mTime is DateTime, true);\n        } catch (e) {\n          fail('An exception was thrown: $e');\n        }\n      });\n      test('exception', () async {\n        final WebDAVClient client = createWebDAVClient();\n        expectLater(\n          () async => await client.getFile(filename: 'unknown.txt'),\n          throwsA(\n            isA<WebDAVClientException>(),\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>12.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import Flutter\nimport UIKit\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    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Ntodotxt</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>ntodotxt</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\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\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n/* 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/* 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\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* 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);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\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\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\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\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\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/* 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\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\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);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* 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/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* 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);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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 = 12.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\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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 = 12.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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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 = 12.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\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt;\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\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt;\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/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* 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      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      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 = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "lib/adaptive_layout/widget/adaptive_layout.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/misc.dart';\nimport 'package:ntodotxt/drawer/widget/drawer.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_state.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/login/state/login_state.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\n\nclass AdaptiveLayout extends StatelessWidget {\n  final Widget child;\n\n  const AdaptiveLayout({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    // if (MediaQuery.of(context).size.width < maxScreenWidthCompact) {\n    // @todo: Activate WideLayout later!\n    return NotificationWrapper(\n      child: NarrowLayout(child: child),\n    );\n    // } else {\n    //   return NotificationWrapper(\n    //     child: WideLayout(child: child),\n    //   );\n    // }\n  }\n}\n\nclass NotificationWrapper extends StatelessWidget {\n  final Widget child;\n\n  const NotificationWrapper({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocListener(\n      listeners: [\n        BlocListener<LoginCubit, LoginState>(\n          listener: (BuildContext context, LoginState state) {\n            if (state is LoginError) {\n              SnackBarHandler.error(context, state.message);\n            }\n          },\n        ),\n        BlocListener<TodoFileCubit, TodoFileState>(\n          listener: (BuildContext context, TodoFileState state) {\n            if (state is TodoFileError) {\n              SnackBarHandler.error(context, state.message);\n            }\n          },\n        ),\n        BlocListener<FilterListBloc, FilterListState>(\n          listener: (BuildContext context, FilterListState state) {\n            if (state is FilterListError) {\n              SnackBarHandler.error(context, state.message);\n            }\n          },\n        ),\n        BlocListener<FilterCubit, FilterState>(\n          listener: (BuildContext context, FilterState state) {\n            if (state is FilterError) {\n              SnackBarHandler.error(context, state.message);\n            }\n          },\n        ),\n        BlocListener<TodoListBloc, TodoListState>(\n          listener: (BuildContext context, TodoListState state) {\n            if (state is TodoListError) {\n              SnackBarHandler.error(context, state.message);\n            }\n          },\n        ),\n      ],\n      child: child,\n    );\n  }\n}\n\nclass NarrowLayout extends StatelessWidget {\n  final Widget child;\n\n  const NarrowLayout({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) => child;\n}\n\nclass WideLayout extends StatelessWidget {\n  final Widget child;\n\n  const WideLayout({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        const NavigationRailDrawer(),\n        const VerticalDivider(width: 2),\n        Expanded(child: child),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/app_info/page/app_details_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/constants/app.dart';\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass AppInfoPage extends StatelessWidget {\n  const AppInfoPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const Scaffold(\n      appBar: MainAppBar(title: 'About'),\n      body: AppInfoView(),\n    );\n  }\n}\n\nclass AppInfoView extends StatelessWidget {\n  static const String repoUrl = 'https://github.com/tmaegel/ntodotxt';\n\n  const AppInfoView({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      children: [\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.update),\n          title: const Text('Version'),\n          subtitle: const Text('v$version'),\n          onTap: () => _openUrl('$repoUrl/blob/main/CHANGELOG.md'),\n        ),\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.code),\n          title: const Text('Source code'),\n          subtitle: const Text(repoUrl),\n          onTap: () => _openUrl(repoUrl),\n        ),\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.bug_report_outlined),\n          title: const Text('Issue tracker'),\n          subtitle: const Text('$repoUrl/issues'),\n          onTap: () => _openUrl('$repoUrl/issues'),\n        ),\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.email_outlined),\n          title: const Text('Contact me'),\n          subtitle: const Text('tnmgl@posteo.de'),\n          onTap: () => _openUrl('mailto:tnmgl@posteo.de'),\n        ),\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.volunteer_activism),\n          title: const Text('Donation'),\n          subtitle: const Text('Support this app'),\n          onTap: () => _openUrl('https://ko-fi.com/tnmgl'),\n        ),\n        ListTile(\n          contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n          leading: const Icon(Icons.shield_outlined),\n          title: const Text('Licence'),\n          subtitle: const Text('MIT License'),\n          onTap: () => context.pushNamed('licenses'),\n        ),\n      ],\n    );\n  }\n\n  Future<void> _openUrl(String urlStr) async {\n    final Uri url = Uri.parse(urlStr);\n    if (!await launchUrl(url)) {\n      throw Exception('Could not open $urlStr');\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/bloc_observer.dart",
    "content": "// coverage:ignore-file\n\nimport 'package:flutter_bloc/flutter_bloc.dart'\n    show Bloc, BlocBase, BlocObserver, Change, Transition;\nimport 'package:ntodotxt/main.dart' show log;\n\nclass GenericBlocObserver extends BlocObserver {\n  @override\n  void onChange(BlocBase bloc, Change change) {\n    super.onChange(bloc, change);\n    log.fine(\n        'STATE CHANGE: ${change.currentState.runtimeType} > ${change.nextState.runtimeType}');\n    log.finer('${bloc.runtimeType} $change');\n  }\n\n  @override\n  void onTransition(Bloc bloc, Transition transition) {\n    super.onTransition(bloc, transition);\n    log.finest('${bloc.runtimeType} $transition');\n  }\n\n  @override\n  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {\n    log.fine('${bloc.runtimeType} $error $stackTrace');\n    super.onError(bloc, error, stackTrace);\n  }\n}\n"
  },
  {
    "path": "lib/common/constants/app.dart",
    "content": "// coverage:ignore-file\n\nconst String version = '0.17.0';\n\n/// https://m3.material.io/foundations/layout/applying-layout/window-size-classes\nconst int maxScreenWidthCompact = 600;\n\nconst String defaultLocalTodoPath = '/';\nconst String defaultRemoteTodoPath = '/';\nconst String defaultTodoFilename = 'todo.txt';\nconst String defaultDoneFilename = 'done.txt';\n"
  },
  {
    "path": "lib/common/exception/exceptions.dart",
    "content": "// coverage:ignore-file\n\nsealed class TodoException implements Exception {\n  final String message;\n\n  const TodoException(this.message);\n\n  @override\n  String toString() => message;\n}\n\nclass TodoNotFound extends TodoException {\n  final String? id;\n  const TodoNotFound({\n    this.id,\n  }) : super('Todo with id $id could not be found');\n}\n\nclass TodoMissingId extends TodoException {\n  const TodoMissingId() : super('Todo has no id key');\n}\n\nclass TodoStringMalformed extends TodoException {\n  final String str;\n  const TodoStringMalformed({\n    required this.str,\n  }) : super('Todo string is malformed: \"$str\"');\n}\n\nclass TodoInvalidProjectTag extends TodoException {\n  final String tag;\n  const TodoInvalidProjectTag({\n    required this.tag,\n  }) : super('Invalid project tag: $tag');\n}\n\nclass TodoInvalidContextTag extends TodoException {\n  final String tag;\n  const TodoInvalidContextTag({\n    required this.tag,\n  }) : super('Invalid context tag: $tag');\n}\n\nclass TodoInvalidKeyValueTag extends TodoException {\n  final String tag;\n  const TodoInvalidKeyValueTag({\n    required this.tag,\n  }) : super('Invalid key value tag: $tag');\n}\n\nclass TodoForbiddenCompletionDate extends TodoException {\n  const TodoForbiddenCompletionDate()\n      : super('Completion date is forbidden if todo is incompleted');\n}\n\nclass TodoMissingCompletionDate extends TodoException {\n  const TodoMissingCompletionDate()\n      : super('Completed todo requires a completion date');\n}\n"
  },
  {
    "path": "lib/common/misc.dart",
    "content": "// coverage:ignore-file\n\nimport 'dart:async';\nimport 'dart:io' show Platform;\n\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\n\nclass PlatformInfo {\n  static bool get isDesktopOS {\n    return Platform.isMacOS || Platform.isLinux || Platform.isWindows;\n  }\n\n  static bool get isAppOS {\n    return Platform.isIOS || Platform.isAndroid;\n  }\n}\n\nenum MessageType { success, info, error }\n\nclass SnackBarHandler {\n  static void _call(BuildContext context, MessageType type, String message) {\n    Color backgroundColor = Theme.of(context).colorScheme.primaryContainer;\n    Color foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer;\n    switch (type) {\n      case MessageType.success:\n        backgroundColor = Theme.of(context).colorScheme.primaryContainer;\n        foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer;\n        break;\n      case MessageType.info:\n        backgroundColor = Theme.of(context).colorScheme.primaryContainer;\n        foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer;\n        break;\n      case MessageType.error:\n        backgroundColor = Theme.of(context).colorScheme.error;\n        foregroundColor = Theme.of(context).colorScheme.onError;\n        break;\n    }\n    ScaffoldMessenger.of(context).showSnackBar(\n      SnackBar(\n        backgroundColor: backgroundColor,\n        duration: type == MessageType.error\n            ? const Duration(seconds: 10)\n            : const Duration(seconds: 4),\n        content: Text(\n          message,\n          style: TextStyle(color: foregroundColor),\n        ),\n      ),\n    );\n  }\n\n  static void success(BuildContext context, String message) =>\n      _call(context, MessageType.success, message);\n\n  static void info(BuildContext context, String message) =>\n      _call(context, MessageType.info, message);\n\n  static void error(BuildContext context, String message) =>\n      _call(context, MessageType.error, message);\n}\n\nclass CustomScrollBehavior extends MaterialScrollBehavior {\n  @override\n  Set<PointerDeviceKind> get dragDevices => {\n        PointerDeviceKind.touch,\n        PointerDeviceKind.mouse,\n        PointerDeviceKind.stylus,\n        PointerDeviceKind.unknown,\n      };\n}\n\nclass Debouncer {\n  Timer? _timer;\n  final int milliseconds;\n\n  Debouncer({required this.milliseconds});\n\n  void run(VoidCallback action) {\n    _timer?.cancel();\n    _timer = Timer(Duration(milliseconds: milliseconds), action);\n  }\n\n  void dispose() {\n    _timer?.cancel();\n    _timer = null;\n  }\n}\n\nclass PopScopeDrawer extends StatelessWidget {\n  final Widget child;\n\n  const PopScopeDrawer({\n    required this.child,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return PopScope(\n      canPop: false,\n      onPopInvokedWithResult: <T>(bool didPop, T? result) {\n        if (didPop) {\n          return;\n        }\n        context.read<DrawerCubit>().back();\n        Navigator.of(context).pop();\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/router/router.dart",
    "content": "// coverage:ignore-file\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/adaptive_layout/widget/adaptive_layout.dart';\nimport 'package:ntodotxt/app_info/page/app_details_page.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/page/filter_create_edit_page.dart';\nimport 'package:ntodotxt/filter/page/filter_list_page.dart';\nimport 'package:ntodotxt/licenses/page/licenses_page.dart';\nimport 'package:ntodotxt/setting/page/settings_page.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/page/todo_create_edit_page.dart';\nimport 'package:ntodotxt/todo/page/todo_list_page.dart';\nimport 'package:ntodotxt/todo/page/todo_search_page.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\n\nclass AppRouter {\n  final GlobalKey<NavigatorState> _rootNavigatorKey =\n      GlobalKey<NavigatorState>(debugLabel: 'root');\n  final GlobalKey<NavigatorState> _shellNavigatorKey =\n      GlobalKey<NavigatorState>(debugLabel: 'shell');\n\n  AppRouter();\n\n  late final GoRouter config = GoRouter(\n    navigatorKey: _rootNavigatorKey,\n    initialLocation: '/todo',\n    debugLogDiagnostics: false,\n    routes: <RouteBase>[\n      ShellRoute(\n        navigatorKey: _shellNavigatorKey,\n        builder: (context, state, child) {\n          return AdaptiveLayout(child: child);\n        },\n        routes: [\n          GoRoute(\n            path: '/settings',\n            name: 'settings',\n            builder: (BuildContext context, GoRouterState state) {\n              return const SettingsPage();\n            },\n            routes: <RouteBase>[\n              GoRoute(\n                path: 'settings/app-info',\n                name: 'app-info',\n                builder: (BuildContext context, GoRouterState state) {\n                  return const AppInfoPage();\n                },\n                routes: <RouteBase>[\n                  GoRoute(\n                    path: 'settings/app-info/licenses',\n                    name: 'licenses',\n                    builder: (BuildContext context, GoRouterState state) {\n                      return const LicenceListPage();\n                    },\n                  ),\n                ],\n              ),\n            ],\n          ),\n          GoRoute(\n            path: '/todo',\n            name: 'todo-list',\n            builder: (BuildContext context, GoRouterState state) {\n              Filter? filter = state.extra as Filter?;\n              return TodoListPage(\n                filter: filter,\n                key: ValueKey(filter?.id ?? 'default'),\n              );\n            },\n            routes: <RouteBase>[\n              GoRoute(\n                path: 'todo/create',\n                name: 'todo-create',\n                builder: (BuildContext context, GoRouterState state) {\n                  Todo todo = state.extra as Todo;\n                  return TodoCreateEditPage(\n                    initTodo: todo,\n                    newTodo: true,\n                    projects: context.read<TodoListBloc>().state.projects,\n                    contexts: context.read<TodoListBloc>().state.contexts,\n                    keyValues: context.read<TodoListBloc>().state.keyValues,\n                  );\n                },\n              ),\n              GoRoute(\n                path: 'todo/edit',\n                name: 'todo-edit',\n                builder: (BuildContext context, GoRouterState state) {\n                  Todo todo = state.extra as Todo;\n                  return TodoCreateEditPage(\n                    initTodo: todo,\n                    newTodo: false,\n                    projects: context.read<TodoListBloc>().state.projects,\n                    contexts: context.read<TodoListBloc>().state.contexts,\n                    keyValues: context.read<TodoListBloc>().state.keyValues,\n                  );\n                },\n              ),\n              GoRoute(\n                path: 'todo/search',\n                name: 'todo-search',\n                builder: (BuildContext context, GoRouterState state) {\n                  Filter? filter = state.extra as Filter?;\n                  return TodoSearchPage(filter: filter);\n                },\n              ),\n            ],\n          ),\n          GoRoute(\n            path: '/filter',\n            name: 'filter-list',\n            builder: (BuildContext context, GoRouterState state) {\n              return const FilterListPage();\n            },\n            routes: <RouteBase>[\n              GoRoute(\n                path: 'filter/create',\n                name: 'filter-create',\n                builder: (BuildContext context, GoRouterState state) {\n                  return FilterCreateEditPage(\n                    projects: context.read<TodoListBloc>().state.projects,\n                    contexts: context.read<TodoListBloc>().state.contexts,\n                  );\n                },\n              ),\n              GoRoute(\n                path: 'filter/edit',\n                name: 'filter-edit',\n                builder: (BuildContext context, GoRouterState state) {\n                  Filter filter = state.extra as Filter;\n                  return FilterCreateEditPage(\n                    initFilter: filter,\n                    projects: context.read<TodoListBloc>().state.projects,\n                    contexts: context.read<TodoListBloc>().state.contexts,\n                  );\n                },\n              ),\n            ],\n          ),\n        ],\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/common/theme/theme.dart",
    "content": "// coverage:ignore-file\n\nimport 'package:flex_color_scheme/flex_color_scheme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:ntodotxt/common/misc.dart';\n\nfinal ThemeData light = CustomTheme.light;\nfinal ThemeData dark = CustomTheme.dark;\n\n/// Customize versions of the theme data.\nfinal ThemeData lightTheme = light.copyWith(\n  appBarTheme: light.appBarTheme.copyWith(\n    backgroundColor: Colors.transparent,\n    systemOverlayStyle: const SystemUiOverlayStyle(\n      statusBarColor: Colors.transparent,\n      statusBarBrightness: Brightness.dark,\n      statusBarIconBrightness: Brightness.dark,\n      systemNavigationBarColor: Colors.transparent,\n      systemNavigationBarIconBrightness: Brightness.dark,\n    ),\n  ),\n  snackBarTheme: light.snackBarTheme.copyWith(\n    elevation: 0.0,\n  ),\n  splashColor: PlatformInfo.isAppOS ? Colors.transparent : null,\n  chipTheme: light.chipTheme.copyWith(),\n  expansionTileTheme: light.expansionTileTheme.copyWith(\n    shape: const Border(),\n    collapsedBackgroundColor: light.appBarTheme.backgroundColor,\n    textColor: light.colorScheme.primary,\n  ),\n  listTileTheme: light.listTileTheme.copyWith(\n    selectedColor: light.textTheme.bodySmall?.color,\n    selectedTileColor: light.hoverColor,\n    contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n    shape: RoundedRectangleBorder(\n      borderRadius: BorderRadius.circular(16),\n    ),\n  ),\n  scrollbarTheme: light.scrollbarTheme.copyWith(\n    thickness: WidgetStateProperty.all(5.0),\n  ),\n  bottomAppBarTheme: light.bottomAppBarTheme.copyWith(),\n  floatingActionButtonTheme: light.floatingActionButtonTheme.copyWith(\n    elevation: 0.0,\n    focusElevation: 0.0,\n    hoverElevation: 0.0,\n  ),\n  inputDecorationTheme: light.inputDecorationTheme.copyWith(\n    filled: false,\n    isDense: true,\n    border: InputBorder.none,\n    enabledBorder: InputBorder.none,\n    focusedBorder: InputBorder.none,\n    focusedErrorBorder: InputBorder.none,\n    errorBorder: InputBorder.none,\n    disabledBorder: InputBorder.none,\n  ),\n  progressIndicatorTheme: light.progressIndicatorTheme.copyWith(\n    color: light.colorScheme.primary,\n    circularTrackColor: light.colorScheme.primaryContainer,\n    refreshBackgroundColor: light.colorScheme.primaryContainer,\n  ),\n);\nfinal ThemeData darkTheme = dark.copyWith(\n  appBarTheme: dark.appBarTheme.copyWith(\n    backgroundColor: Colors.transparent,\n    systemOverlayStyle: const SystemUiOverlayStyle(\n      statusBarColor: Colors.transparent,\n      statusBarBrightness: Brightness.light,\n      statusBarIconBrightness: Brightness.light,\n      systemNavigationBarColor: Colors.transparent,\n      systemNavigationBarIconBrightness: Brightness.light,\n    ),\n  ),\n  snackBarTheme: dark.snackBarTheme.copyWith(\n    elevation: 0.0,\n  ),\n  splashColor: PlatformInfo.isAppOS ? Colors.transparent : null,\n  chipTheme: dark.chipTheme.copyWith(),\n  expansionTileTheme: dark.expansionTileTheme.copyWith(\n    shape: const Border(),\n    collapsedBackgroundColor: dark.appBarTheme.backgroundColor,\n    textColor: dark.colorScheme.primary,\n  ),\n  listTileTheme: dark.listTileTheme.copyWith(\n    selectedColor: dark.textTheme.bodySmall?.color,\n    selectedTileColor: dark.hoverColor,\n    contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n    shape: RoundedRectangleBorder(\n      borderRadius: BorderRadius.circular(16),\n    ),\n  ),\n  scrollbarTheme: dark.scrollbarTheme.copyWith(\n    thickness: WidgetStateProperty.all(5.0),\n  ),\n  bottomAppBarTheme: dark.bottomAppBarTheme.copyWith(),\n  floatingActionButtonTheme: dark.floatingActionButtonTheme.copyWith(\n    elevation: 0.0,\n    focusElevation: 0.0,\n    hoverElevation: 0.0,\n  ),\n  inputDecorationTheme: dark.inputDecorationTheme.copyWith(\n    filled: false,\n    isDense: true,\n    border: InputBorder.none,\n    enabledBorder: InputBorder.none,\n    focusedBorder: InputBorder.none,\n    focusedErrorBorder: InputBorder.none,\n    errorBorder: InputBorder.none,\n    disabledBorder: InputBorder.none,\n  ),\n  progressIndicatorTheme: dark.progressIndicatorTheme.copyWith(\n    color: dark.colorScheme.primary,\n    circularTrackColor: dark.colorScheme.primaryContainer,\n    refreshBackgroundColor: dark.colorScheme.primaryContainer,\n  ),\n);\n\n// Theme config for FlexColorScheme version 7.3.x. Make sure you use\n// same or higher package version, but still same major version. If you\n// use a lower package version, some properties may not be supported.\n// In that case remove them after copying this theme to your app.\nclass CustomTheme {\n  static ThemeData get light {\n    return FlexThemeData.light(\n      scheme: FlexScheme.bahamaBlue,\n      surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold,\n      blendLevel: 7,\n      subThemesData: const FlexSubThemesData(\n        blendOnLevel: 10,\n        blendOnColors: false,\n        useM2StyleDividerInM3: true,\n        alignedDropdown: true,\n        useInputDecoratorThemeInDialogs: true,\n      ),\n      visualDensity: FlexColorScheme.comfortablePlatformDensity,\n      useMaterial3: true,\n      swapLegacyOnMaterial3: true,\n      fontFamily: 'OpenSans',\n    );\n  }\n\n  static ThemeData get dark {\n    return FlexThemeData.dark(\n      scheme: FlexScheme.bahamaBlue,\n      surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold,\n      blendLevel: 13,\n      subThemesData: const FlexSubThemesData(\n        blendOnLevel: 20,\n        useM2StyleDividerInM3: true,\n        alignedDropdown: true,\n        useInputDecoratorThemeInDialogs: true,\n      ),\n      visualDensity: FlexColorScheme.comfortablePlatformDensity,\n      useMaterial3: true,\n      swapLegacyOnMaterial3: true,\n      fontFamily: 'OpenSans',\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/app_bar.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/misc.dart' show CustomScrollBehavior;\nimport 'package:ntodotxt/drawer/widget/drawer.dart';\nimport 'package:ntodotxt/filter/widget/filter_chip.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\n\nclass MainAppBar extends StatelessWidget implements PreferredSizeWidget {\n  final String title;\n  final Widget? toolbar;\n  final Widget? bottom;\n\n  const MainAppBar({\n    required this.title,\n    this.toolbar,\n    this.bottom,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    // @todo: Activate WideLayout later!\n    // final bool narrowView =\n    //     MediaQuery.of(context).size.width < maxScreenWidthCompact;\n    return AppBar(\n      // titleSpacing: narrowView ? 0.0 : null,\n      titleSpacing: 0.0,\n      title: Text(title),\n      // leading: narrowView && Scaffold.of(context).hasDrawer\n      leading: Scaffold.of(context).hasDrawer\n          ? Builder(\n              builder: (BuildContext context) {\n                return IconButton(\n                  tooltip: 'Open drawer',\n                  icon: const Icon(Icons.menu),\n                  onPressed: () async {\n                    await showModalBottomSheet(\n                      context: context,\n                      isScrollControlled: true,\n                      builder: (BuildContext context) =>\n                          const BottomSheetNavigationDrawer(),\n                    );\n                  },\n                );\n              },\n            )\n          : null,\n      actions: toolbar == null\n          ? null\n          : <Widget>[\n              toolbar!,\n              const SizedBox(width: 8),\n            ],\n      bottom: bottom == null\n          ? null\n          : PreferredSize(\n              preferredSize: Size.zero,\n              child: Padding(\n                padding: const EdgeInsets.only(bottom: 8.0),\n                child: bottom!,\n              ),\n            ),\n    );\n  }\n\n  // Scaffold requires as appbar a class that implements PreferredSizeWidget.\n  @override\n  Size get preferredSize =>\n      Size.fromHeight(bottom == null ? kToolbarHeight : 110);\n}\n\nclass AppBarFilterList extends StatelessWidget {\n  const AppBarFilterList({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final ScrollController controller = ScrollController();\n\n    return BlocBuilder<TodoListBloc, TodoListState>(\n      builder: (BuildContext context, TodoListState todoListState) {\n        return ScrollConfiguration(\n          behavior: CustomScrollBehavior(),\n          child: SingleChildScrollView(\n            controller: controller,\n            scrollDirection: Axis.horizontal,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16.0),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  const FilterOrderChip(),\n                  const SizedBox(width: 4),\n                  const FilterFilterChip(),\n                  const SizedBox(width: 4),\n                  const FilterGroupChip(),\n                  const SizedBox(width: 4),\n                  const FilterPrioritiesChip(),\n                  const SizedBox(width: 4),\n                  FilterProjectsChip(availableTags: todoListState.projects),\n                  const SizedBox(width: 4),\n                  FilterContextsChip(availableTags: todoListState.contexts),\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/chip.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass BasicIconChip extends StatelessWidget {\n  final String label;\n  final IconData iconData;\n  final bool mono;\n\n  const BasicIconChip({\n    required this.label,\n    required this.iconData,\n    this.mono = false,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 0.0),\n      decoration: BoxDecoration(\n        color: mono\n            ? Theme.of(context).colorScheme.surfaceContainerHigh\n            : Theme.of(context).colorScheme.secondaryContainer,\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: <Widget>[\n          Icon(iconData, size: 14.0),\n          const SizedBox(width: 2.0),\n          Text(\n            label,\n            style: TextStyle(\n              color: mono\n                  ? Theme.of(context).colorScheme.onSurfaceVariant\n                  : Theme.of(context).colorScheme.onSecondaryContainer,\n            ),\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass BasicChip extends StatelessWidget {\n  final String label;\n  final bool mono;\n\n  const BasicChip({\n    required this.label,\n    this.mono = false,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 0.0),\n      decoration: BoxDecoration(\n        color: mono\n            ? Theme.of(context).colorScheme.surfaceContainerHigh\n            : Theme.of(context).colorScheme.secondaryContainer,\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: Text(\n        label,\n        style: TextStyle(\n          color: mono\n              ? Theme.of(context).colorScheme.onSurfaceVariant\n              : Theme.of(context).colorScheme.onSecondaryContainer,\n        ),\n      ),\n    );\n  }\n}\n\nclass GenericActionChip extends StatelessWidget {\n  final Widget label;\n  final Widget avatar;\n  final Function() onPressed;\n  final bool selected;\n\n  const GenericActionChip({\n    required this.label,\n    required this.avatar,\n    required this.onPressed,\n    this.selected = false,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ActionChip(\n      avatar: avatar,\n      label: label,\n      padding: EdgeInsets.zero,\n      side: selected == true\n          ? BorderSide(color: Theme.of(context).colorScheme.primary)\n          : null,\n      labelPadding: const EdgeInsets.only(right: 8.0),\n      onPressed: () async => onPressed(),\n    );\n  }\n}\n\nclass GenericChoiceChip extends StatelessWidget {\n  final Widget label;\n  final bool selected;\n  final bool showCheckmark;\n  final Function(bool selected) onSelected;\n\n  const GenericChoiceChip({\n    required this.label,\n    this.selected = false,\n    this.showCheckmark = false,\n    required this.onSelected,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ChoiceChip(\n      label: label,\n      selected: selected,\n      showCheckmark: showCheckmark,\n      // Workaround: https://github.com/flutter/flutter/issues/67797\n      visualDensity: const VisualDensity(\n        horizontal: -4.0,\n        vertical: -4.0,\n      ),\n      onSelected: (bool selected) => onSelected(selected),\n    );\n  }\n}\n\nclass GenericChipGroup extends StatelessWidget {\n  final List<Widget> children;\n  final WrapAlignment alignment;\n\n  const GenericChipGroup({\n    required this.children,\n    this.alignment = WrapAlignment.start,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Wrap(\n      spacing: 4.0, // gap between adjacent chips\n      alignment: alignment,\n      runSpacing: 4.0, // gap between lines\n      runAlignment: WrapAlignment.center,\n      crossAxisAlignment: WrapCrossAlignment.start,\n      children: children,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/confirm_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ConfirmationDialog extends StatelessWidget {\n  final String title;\n  final String message;\n  final String actionLabel;\n  final String cancelLabel;\n\n  const ConfirmationDialog({\n    required this.title,\n    required this.message,\n    required this.actionLabel,\n    required this.cancelLabel,\n    super.key,\n  });\n\n  static Future<bool> dialog({\n    required BuildContext context,\n    required String title,\n    required String message,\n    required String actionLabel,\n    required String cancelLabel,\n  }) async {\n    bool? result = await showDialog<bool>(\n      useRootNavigator: false,\n      context: context,\n      barrierDismissible: false, // User must tap button.\n      builder: (BuildContext context) => ConfirmationDialog(\n        title: title,\n        message: message,\n        actionLabel: actionLabel,\n        cancelLabel: cancelLabel,\n      ),\n    );\n\n    return result ?? false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      title: Text(title),\n      content: Text(message),\n      actions: <Widget>[\n        TextButton(\n          child: Text(cancelLabel),\n          onPressed: () => Navigator.pop(context, false),\n        ),\n        TextButton(\n          child: Text(actionLabel),\n          onPressed: () => Navigator.pop(context, true),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/contexts_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/widget/tag_dialog.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\n\nclass FilterContextTagDialog extends TagDialog {\n  final FilterCubit cubit;\n\n  const FilterContextTagDialog({\n    required this.cubit,\n    super.title = 'Contexts',\n    super.tagName = 'context',\n    super.availableTags,\n    super.addTags = false,\n    super.key = const Key('FilterContextTagDialog'),\n  });\n\n  @override\n  RegExp get regex => RegExp(r'^\\S+$');\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n    required Set<String> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => FilterContextTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<FilterContextTagDialog> createState() => _FilterContextTagDialogState();\n}\n\nclass _FilterContextTagDialogState\n    extends TagDialogState<FilterContextTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (String t) => Tag(\n          name: t,\n          selected: widget.cubit.state.filter.contexts.contains(t),\n        ),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate() {\n    widget.cubit.updateContexts({\n      for (Tag t in tags)\n        if (t.selected) t.name\n    });\n  }\n}\n\nclass TodoContextTagDialog extends TagDialog {\n  final TodoCubit cubit;\n\n  const TodoContextTagDialog({\n    required this.cubit,\n    super.title = 'Contexts',\n    super.tagName = 'context',\n    super.availableTags,\n    super.addTags = true,\n    super.key = const Key('TodoContextTagDialog'),\n  });\n\n  @override\n  RegExp get regex => RegExp(r'^\\S+$');\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required TodoCubit cubit,\n    required Set<String> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => TodoContextTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<TodoContextTagDialog> createState() => _TodoContextTagDialogState();\n}\n\nclass _TodoContextTagDialogState extends TagDialogState<TodoContextTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (String t) => Tag(\n          name: t,\n          selected: widget.cubit.state.todo.contexts.contains(t),\n        ),\n      ),\n      // Overwrites contexts of todo with selected=true\n      ...widget.cubit.state.todo.contexts.map(\n        (String t) => Tag(name: t, selected: true),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate() {\n    widget.cubit.updateContexts({\n      for (Tag t in tags)\n        if (t.selected) t.name\n    });\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/date_picker.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass TodoDatePicker {\n  static final int defaultDaysOffset = 3650;\n\n  const TodoDatePicker();\n\n  static Future<DateTime?> pickDate({\n    required BuildContext context,\n    required DateTime? initialDate,\n    int? startDateDaysOffset,\n    int? endDateDaysOffset,\n  }) async {\n    final DateTime initial = initialDate ?? DateTime.now();\n\n    return await showDatePicker(\n      useRootNavigator: false,\n      context: context,\n      firstDate: initial.subtract(\n        Duration(days: startDateDaysOffset ?? defaultDaysOffset),\n      ),\n      initialDate: initial,\n      lastDate: initial.add(\n        Duration(days: endDateDaysOffset ?? defaultDaysOffset),\n      ),\n      locale: const Locale('en', 'GB'),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/filter_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show ListFilter;\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\n\nclass FilterStateFilterDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListFilter> items;\n\n  const FilterStateFilterDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'All': ListFilter.all,\n          'Completed only': ListFilter.completedOnly,\n          'Incompleted only': ListFilter.incompletedOnly,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) => FilterStateFilterDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListFilter value = items[key]!;\n          return RadioListTile<ListFilter>(\n            key: Key('${value.name}DialogRadioButton'),\n            contentPadding: EdgeInsets.zero,\n            title: Text(key),\n            value: value,\n            groupValue: cubit.state.filter.filter,\n            onChanged: (ListFilter? value) {\n              if (value != null) {\n                cubit.updateFilter(value);\n              }\n              Navigator.pop(context);\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DefaultFilterStateFilterDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListFilter> items;\n\n  const DefaultFilterStateFilterDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'All': ListFilter.all,\n          'Completed only': ListFilter.completedOnly,\n          'Incompleted only': ListFilter.incompletedOnly,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) =>\n          DefaultFilterStateFilterDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListFilter value = items[key]!;\n          return RadioListTile<ListFilter>(\n            key: Key('${value.name}DialogRadioButton'),\n            contentPadding: EdgeInsets.zero,\n            value: value,\n            title: Text(key),\n            groupValue: cubit.state.filter.filter,\n            onChanged: (ListFilter? value) {\n              if (value != null) {\n                cubit.updateDefaultFilter(value);\n              }\n              Navigator.pop(context);\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/group_by_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show ListGroup;\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\n\nclass FilterStateGroupDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListGroup> items;\n\n  const FilterStateGroupDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'None': ListGroup.none,\n          'Upcoming': ListGroup.upcoming,\n          'Priority': ListGroup.priority,\n          'Project': ListGroup.project,\n          'Context': ListGroup.context,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) => FilterStateGroupDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListGroup value = items[key]!;\n          return RadioListTile<ListGroup>(\n            key: Key('${value.name}DialogRadioButton'),\n            contentPadding: EdgeInsets.zero,\n            title: Text(key),\n            value: value,\n            groupValue: cubit.state.filter.group,\n            onChanged: (ListGroup? value) {\n              if (value != null) {\n                cubit.updateGroup(value);\n              }\n              Navigator.pop(context);\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DefaultFilterStateGroupDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListGroup> items;\n\n  const DefaultFilterStateGroupDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'None': ListGroup.none,\n          'Upcoming': ListGroup.upcoming,\n          'Priority': ListGroup.priority,\n          'Project': ListGroup.project,\n          'Context': ListGroup.context,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) =>\n          DefaultFilterStateGroupDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListGroup value = items[key]!;\n          return RadioListTile<ListGroup>(\n              key: Key('${value.name}DialogRadioButton'),\n              contentPadding: EdgeInsets.zero,\n              value: value,\n              title: Text(key),\n              groupValue: cubit.state.filter.group,\n              onChanged: (ListGroup? value) {\n                if (value != null) {\n                  cubit.updateDefaultGroup(value);\n                }\n                Navigator.pop(context);\n              });\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/info_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass InfoDialog extends StatelessWidget {\n  final String title;\n  final String message;\n\n  const InfoDialog({\n    required this.title,\n    required this.message,\n    super.key,\n  });\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required String title,\n    required String message,\n  }) async {\n    return await showDialog<void>(\n      useRootNavigator: false,\n      context: context,\n      barrierDismissible: true,\n      builder: (BuildContext context) => InfoDialog(\n        title: title,\n        message: message,\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AlertDialog(\n      title: Text(title),\n      titleTextStyle: Theme.of(context).textTheme.titleMedium,\n      content: Text(message),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/input_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass InputDialog extends StatelessWidget {\n  final String title;\n  final String label;\n  final String? value;\n\n  const InputDialog({\n    required this.title,\n    required this.label,\n    this.value,\n    super.key,\n  });\n\n  static Future<String?> dialog({\n    required BuildContext context,\n    required String title,\n    required String label,\n    String? value,\n  }) async {\n    return await showDialog<String?>(\n      useRootNavigator: false,\n      context: context,\n      barrierDismissible: false, // User must tap button.\n      builder: (BuildContext context) =>\n          InputDialog(title: title, label: label, value: value),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final TextEditingController controller = TextEditingController();\n    controller.text = value ?? '';\n    return AlertDialog(\n      title: Text(title),\n      content: TextField(\n        controller: controller,\n        decoration: InputDecoration(hintText: label),\n      ),\n      actions: <Widget>[\n        TextButton(\n          child: const Text('Cancel'),\n          onPressed: () => Navigator.pop(context),\n        ),\n        TextButton(\n          child: const Text('Ok'),\n          onPressed: () => Navigator.pop(context, controller.text),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/key_values_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/widget/tag_dialog.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\n\nclass TodoKeyValueTagDialog extends TagDialog {\n  final TodoCubit cubit;\n\n  const TodoKeyValueTagDialog({\n    required this.cubit,\n    super.title = 'Key values',\n    super.tagName = 'key:value',\n    super.availableTags,\n    super.addTags = true,\n    super.key = const Key('TodoKeyValueTagDialog'),\n  });\n\n  @override\n  RegExp get regex => Todo.patternKeyValue;\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required TodoCubit cubit,\n    required Set<String> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => TodoKeyValueTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<TodoKeyValueTagDialog> createState() => _TodoKeyValueTagDialogState();\n}\n\nclass _TodoKeyValueTagDialogState\n    extends TagDialogState<TodoKeyValueTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (String t) => Tag(\n          name: t,\n          selected: widget.cubit.state.todo.fmtKeyValues.contains(t),\n        ),\n      ),\n      // Overwrites key values of todo with selected=true\n      ...widget.cubit.state.todo.fmtKeyValues.map(\n        (String t) => Tag(name: t, selected: true),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate() {\n    widget.cubit.updateKeyValues({\n      for (Tag t in tags)\n        if (t.selected) t.name\n    });\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/order_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show ListOrder;\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\n\nclass FilterStateOrderDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListOrder> items;\n\n  const FilterStateOrderDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'Ascending': ListOrder.ascending,\n          'Descending': ListOrder.descending,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) => FilterStateOrderDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListOrder value = items[key]!;\n          return RadioListTile<ListOrder>(\n            key: Key('${value.name}DialogRadioButton'),\n            contentPadding: EdgeInsets.zero,\n            title: Text(key),\n            value: value,\n            groupValue: cubit.state.filter.order,\n            onChanged: (ListOrder? value) {\n              if (value != null) {\n                cubit.updateOrder(value);\n              }\n              Navigator.pop(context);\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DefaultFilterStateOrderDialog extends StatelessWidget {\n  final FilterCubit cubit;\n  final Map<String, ListOrder> items;\n\n  const DefaultFilterStateOrderDialog({\n    required this.cubit,\n    super.key,\n  }) : items = const {\n          'Ascending': ListOrder.ascending,\n          'Descending': ListOrder.descending,\n        };\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n  }) async {\n    return await showDialog<Future<void>>(\n      useRootNavigator: false,\n      context: context,\n      builder: (BuildContext context) =>\n          DefaultFilterStateOrderDialog(cubit: cubit),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      child: ListView.builder(\n        shrinkWrap: true,\n        padding: const EdgeInsets.all(16.0),\n        itemCount: items.length,\n        itemBuilder: (BuildContext context, int index) {\n          String key = items.keys.elementAt(index);\n          ListOrder value = items[key]!;\n          return RadioListTile<ListOrder>(\n            key: Key('${value.name}DialogRadioButton'),\n            contentPadding: EdgeInsets.zero,\n            value: value,\n            title: Text(key),\n            groupValue: cubit.state.filter.order,\n            onChanged: (ListOrder? value) {\n              if (value != null) {\n                cubit.updateDefaultOrder(value);\n              }\n              Navigator.pop(context);\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/priorities_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/misc.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\n\nclass PriorityTag {\n  Priority priority;\n  bool selected;\n\n  PriorityTag({\n    required this.priority,\n    required this.selected,\n  });\n\n  @override\n  String toString() => priority.name;\n}\n\nclass PriorityTagDialog extends StatefulWidget {\n  final String title;\n  final Set<Priority> availableTags;\n\n  const PriorityTagDialog({\n    required this.title,\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  State<PriorityTagDialog> createState() => PriorityTagDialogState();\n}\n\nclass PriorityTagDialogState<T extends PriorityTagDialog> extends State<T> {\n  // Holds the selected tags before adding to the regular state.\n  Set<PriorityTag> tags = {};\n\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    // Clean up the controller when the widget is disposed.\n    _controller.dispose();\n    super.dispose();\n  }\n\n  Set<PriorityTag> get sortedTags {\n    List<PriorityTag> t = tags.toList()\n      ..sort(\n        (PriorityTag a, PriorityTag b) => a.toString().compareTo(b.toString()),\n      );\n    return t.toSet();\n  }\n\n  void onUpdate(PriorityTag value, bool selected) {}\n\n  @override\n  Widget build(BuildContext context) {\n    return DraggableScrollableSheet(\n      initialChildSize: 0.5,\n      minChildSize: 0.15,\n      maxChildSize: 0.9,\n      expand: false,\n      builder: (BuildContext context, ScrollController scrollController) {\n        return ScrollConfiguration(\n          behavior: CustomScrollBehavior(),\n          child: ListView(\n            controller: scrollController,\n            children: [\n              Padding(\n                padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n                child: ListTile(\n                  contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  title: Text(\n                    widget.title,\n                    style: Theme.of(context).textTheme.titleLarge,\n                  ),\n                ),\n              ),\n              if (tags.isNotEmpty) const Divider(),\n              if (tags.isNotEmpty)\n                Padding(\n                  padding: const EdgeInsets.symmetric(\n                      horizontal: 16.0, vertical: 8.0),\n                  child: ListTile(\n                    contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n                    title: GenericChipGroup(\n                      children: [\n                        for (var t in sortedTags)\n                          GenericChoiceChip(\n                            label: Text(t.priority.name),\n                            selected: t.selected,\n                            onSelected: (bool selected) =>\n                                onUpdate(t, selected),\n                          ),\n                      ],\n                    ),\n                  ),\n                ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterPriorityTagDialog extends PriorityTagDialog {\n  final FilterCubit cubit;\n\n  const FilterPriorityTagDialog({\n    required this.cubit,\n    super.title = 'Priorities',\n    super.availableTags,\n    super.key = const Key('FilterPriorityTagDialog'),\n  });\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n    required Set<Priority> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => FilterPriorityTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<FilterPriorityTagDialog> createState() =>\n      _FilterPriorityTagDialogState();\n}\n\nclass _FilterPriorityTagDialogState\n    extends PriorityTagDialogState<FilterPriorityTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (Priority t) => PriorityTag(\n          priority: t,\n          selected: widget.cubit.state.filter.priorities.contains(t),\n        ),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate(PriorityTag value, bool selected) {\n    setState(() {\n      value.selected = selected;\n    });\n    widget.cubit.updatePriorities({\n      for (PriorityTag t in tags)\n        if (t.selected) t.priority\n    });\n  }\n}\n\nclass TodoPriorityTagDialog extends PriorityTagDialog {\n  final TodoCubit cubit;\n\n  const TodoPriorityTagDialog({\n    required this.cubit,\n    super.title = 'Priorities',\n    super.availableTags,\n    super.key = const Key('TodoPriorityTagDialog'),\n  });\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required TodoCubit cubit,\n    required Set<Priority> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => TodoPriorityTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<TodoPriorityTagDialog> createState() => _TodoPriorityTagDialogState();\n}\n\nclass _TodoPriorityTagDialogState\n    extends PriorityTagDialogState<TodoPriorityTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (Priority t) => PriorityTag(\n          priority: t,\n          selected: widget.cubit.state.todo.priority == t,\n        ),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate(PriorityTag value, bool selected) {\n    setState(() {\n      // Unset priorities first.\n      for (PriorityTag tag in tags) {\n        tag.selected = false;\n      }\n      value.selected = selected;\n    });\n    if (selected) {\n      widget.cubit.setPriority(value.priority);\n    } else {\n      widget.cubit.unsetPriority();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/projects_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/widget/tag_dialog.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\n\nclass FilterProjectTagDialog extends TagDialog {\n  final FilterCubit cubit;\n\n  const FilterProjectTagDialog({\n    required this.cubit,\n    super.title = 'Projects',\n    super.tagName = 'project',\n    super.availableTags,\n    super.addTags = false,\n    super.key = const Key('FilterProjectTagDialog'),\n  });\n\n  @override\n  RegExp get regex => RegExp(r'^\\S+$');\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required FilterCubit cubit,\n    required Set<String> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => FilterProjectTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<FilterProjectTagDialog> createState() => _FilterProjectTagDialogState();\n}\n\nclass _FilterProjectTagDialogState\n    extends TagDialogState<FilterProjectTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (String t) => Tag(\n          name: t,\n          selected: widget.cubit.state.filter.projects.contains(t),\n        ),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate() {\n    widget.cubit.updateProjects({\n      for (Tag t in tags)\n        if (t.selected) t.name\n    });\n  }\n}\n\nclass TodoProjectTagDialog extends TagDialog {\n  final TodoCubit cubit;\n\n  const TodoProjectTagDialog({\n    required this.cubit,\n    super.title = 'Projects',\n    super.tagName = 'project',\n    super.availableTags,\n    super.addTags = true,\n    super.key = const Key('TodoProjectTagDialog'),\n  });\n\n  @override\n  RegExp get regex => RegExp(r'^\\S+$');\n\n  static Future<void> dialog({\n    required BuildContext context,\n    required TodoCubit cubit,\n    required Set<String> availableTags,\n  }) async {\n    showModalBottomSheet<void>(\n      context: context,\n      isScrollControlled: true,\n      builder: (BuildContext context) => TodoProjectTagDialog(\n        cubit: cubit,\n        availableTags: availableTags,\n      ),\n    );\n  }\n\n  @override\n  State<TodoProjectTagDialog> createState() => _TodoProjectTagDialogState();\n}\n\nclass _TodoProjectTagDialogState extends TagDialogState<TodoProjectTagDialog> {\n  @override\n  void initState() {\n    super.initState();\n    super.tags = {\n      ...widget.availableTags.map(\n        (String t) => Tag(\n          name: t,\n          selected: widget.cubit.state.todo.projects.contains(t),\n        ),\n      ),\n      // Overwrites projects of todo with selected=true\n      ...widget.cubit.state.todo.projects.map(\n        (String t) => Tag(name: t, selected: true),\n      ),\n    };\n  }\n\n  @override\n  void onUpdate() {\n    widget.cubit.updateProjects({\n      for (Tag t in tags)\n        if (t.selected) t.name\n    });\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/scroll_to_top.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ScollToTopView extends StatefulWidget {\n  const ScollToTopView({super.key});\n\n  @override\n  State<ScollToTopView> createState() => ScollToTopViewState();\n}\n\nclass ScollToTopViewState<T extends ScollToTopView> extends State<T> {\n  bool scrolledDown = false;\n  late ScrollController scrollController;\n\n  @override\n  void initState() {\n    scrollController = ScrollController()\n      ..addListener(\n        () {\n          setState(() {\n            if (scrollController.offset >= 50) {\n              scrolledDown = true;\n            } else {\n              scrolledDown = false;\n            }\n          });\n        },\n      );\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  void scrollToTop() {\n    scrollController.animateTo(0,\n        duration: const Duration(milliseconds: 250), curve: Curves.linear);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container();\n  }\n}\n"
  },
  {
    "path": "lib/common/widget/tag_dialog.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/misc.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\n\nclass Tag {\n  final String name;\n  bool selected;\n\n  Tag({\n    required this.name,\n    required this.selected,\n  });\n\n  // Makes overwriting possible if 'selected' is different.\n  @override\n  bool operator ==(Object other) => other is Tag && name == other.name;\n\n  @override\n  int get hashCode => name.hashCode;\n\n  @override\n  String toString() => '$name ($selected)';\n}\n\nclass TagDialog extends StatefulWidget {\n  final String title;\n  final String tagName;\n  final Set<String> availableTags;\n  final bool addTags;\n\n  const TagDialog({\n    required this.title,\n    required this.tagName,\n    this.availableTags = const {},\n    this.addTags = true,\n    super.key,\n  });\n\n  RegExp get regex => RegExp(r'^\\S+$');\n\n  @override\n  State<TagDialog> createState() => TagDialogState();\n}\n\nclass TagDialogState<T extends TagDialog> extends State<T> {\n  // Holds the selected tags before adding to the regular state.\n  Set<Tag> tags = {};\n\n  late GlobalKey<FormState> _formKey;\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _formKey = GlobalKey<FormState>();\n    _controller = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    // Clean up the controller when the widget is disposed.\n    _controller.dispose();\n    super.dispose();\n  }\n\n  Set<Tag> get sortedTags {\n    List<Tag> t = tags.toList()\n      ..sort(\n        (Tag a, Tag b) => a.toString().compareTo(b.toString()),\n      );\n    return t.toSet();\n  }\n\n  void onUpdate() {}\n\n  @override\n  Widget build(BuildContext context) {\n    return DraggableScrollableSheet(\n      initialChildSize: widget.addTags == true ? 0.9 : 0.5,\n      minChildSize: 0.15,\n      maxChildSize: 0.9,\n      expand: false,\n      builder: (BuildContext context, ScrollController scrollController) {\n        return ScrollConfiguration(\n          behavior: CustomScrollBehavior(),\n          child: ListView(\n            controller: scrollController,\n            children: [\n              Padding(\n                padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n                child: ListTile(\n                  contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  title: Text(\n                    widget.title,\n                    style: Theme.of(context).textTheme.titleLarge,\n                  ),\n                ),\n              ),\n              const Divider(),\n              if (widget.addTags)\n                Padding(\n                  padding: const EdgeInsets.symmetric(\n                      horizontal: 16.0, vertical: 8.0),\n                  child: ListTile(\n                    contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),\n                    title: Form(\n                      key: _formKey,\n                      child: TextFormField(\n                        controller: _controller,\n                        style: Theme.of(context).textTheme.bodyMedium,\n                        textCapitalization: TextCapitalization.sentences,\n                        decoration: InputDecoration(\n                          hintText: 'Enter <${widget.tagName}> tag ...',\n                          contentPadding: EdgeInsets.zero,\n                        ),\n                        validator: (String? value) {\n                          if (value == null || value.isEmpty) {\n                            return 'Missing tag name';\n                          }\n                          if (!widget.regex.hasMatch(value.trim())) {\n                            return 'Invalid tag format';\n                          }\n                          return null;\n                        },\n                      ),\n                    ),\n                    trailing: TextButton(\n                      child: const Text('Add'),\n                      onPressed: () {\n                        if (_formKey.currentState!.validate()) {\n                          String text = _controller.text.trim();\n                          if (text.startsWith('+') || text.startsWith('@')) {\n                            text = text.substring(1);\n                          }\n                          setState(() {\n                            tags.add(Tag(name: text, selected: true));\n                          });\n                          _controller.text = '';\n                          onUpdate();\n                        }\n                      },\n                    ),\n                  ),\n                ),\n              if (widget.addTags) const Divider(),\n              tags.isEmpty\n                  ? Padding(\n                      padding: const EdgeInsets.symmetric(\n                        horizontal: 16.0,\n                        vertical: 8.0,\n                      ),\n                      child: ListTile(\n                        contentPadding: const EdgeInsets.symmetric(\n                          horizontal: 8.0,\n                        ),\n                        title: Text('No ${widget.tagName} tags available.'),\n                      ),\n                    )\n                  : Padding(\n                      padding: const EdgeInsets.symmetric(\n                          horizontal: 16.0, vertical: 8.0),\n                      child: ListTile(\n                        contentPadding:\n                            const EdgeInsets.symmetric(horizontal: 8.0),\n                        title: GenericChipGroup(\n                          children: [\n                            for (var t in sortedTags)\n                              GenericChoiceChip(\n                                label: Text(t.name),\n                                selected: t.selected,\n                                onSelected: (bool selected) {\n                                  setState(() {\n                                    t.selected = selected;\n                                  });\n                                  onUpdate();\n                                },\n                              ),\n                          ],\n                        ),\n                      ),\n                    ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/database/controller/database.dart",
    "content": "import 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/main.dart' show log;\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nabstract class ModelControllerInterface<T> {\n  Future<List<T>> list();\n\n  Future<T?> get(dynamic identifier);\n\n  Future<int> insert(T model);\n\n  Future<int> update(T model);\n\n  Future<int> delete(dynamic identifier);\n}\n\nclass DatabaseController {\n  static Database? _database; // Singleton pattern\n  final String path;\n\n  const DatabaseController(this.path);\n\n  Future<Database> get database async {\n    if (_database != null) {\n      return _database!;\n    } else {\n      _database = await _open();\n      return _database!;\n    }\n  }\n\n  Future<void> close() async {\n    if (_database != null) {\n      await _database!.close();\n    }\n    _database = null;\n  }\n\n  Future<Database> _open() async {\n    // Change the default factory. On iOS/Android, if not using `sqlite_flutter_lib`\n    // you can forget this step, it will use the sqlite version available on the system.\n    databaseFactoryOrNull = databaseFactoryFfi;\n    return openDatabase(\n      path,\n      // Set the version. This executes the onCreate function and provides a\n      // path to perform database upgrades and downgrades.\n      version: 1,\n      onCreate: (Database db, int version) {\n        log.info('Create database $path');\n        db.execute(Setting.tableRepr);\n        db.execute(Filter.tableRepr);\n      },\n      onUpgrade: (Database db, int oldVersion, int newVersion) {\n        if (newVersion > oldVersion) {\n          log.info('Perform database upgrade');\n        }\n      },\n      onOpen: (Database db) {},\n      singleInstance: true,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/drawer/state/drawer_cubit.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/drawer/state/drawer_state.dart';\n\n/// Keep the state of the selected item within the drawer.\nclass DrawerCubit extends Cubit<DrawerState> {\n  DrawerCubit() : super(const DrawerState(index: 0));\n\n  void reset() => emit(const DrawerState(index: 0));\n\n  void next(int index) {\n    emit(\n      DrawerState(\n        prevIndex: state.index,\n        index: index,\n      ),\n    );\n  }\n\n  void back() {\n    emit(\n      DrawerState(\n        prevIndex: null,\n        index: state.prevIndex ?? 0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/drawer/state/drawer_state.dart",
    "content": "import 'package:equatable/equatable.dart';\n\nfinal class DrawerState extends Equatable {\n  final int index;\n  final int? prevIndex;\n\n  const DrawerState({\n    required this.index,\n    this.prevIndex,\n  });\n\n  @override\n  List<Object?> get props => [\n        index,\n        prevIndex,\n      ];\n\n  @override\n  String toString() => 'DrawerState { index: $index prevIndex: $prevIndex }';\n}\n"
  },
  {
    "path": "lib/drawer/widget/drawer.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/drawer/state/drawer_state.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_state.dart';\n\nclass DrawerDestination {\n  final String label;\n  final Widget icon;\n  final Widget selectedIcon;\n  final Function(BuildContext context) onTap;\n\n  const DrawerDestination({\n    required this.label,\n    required this.icon,\n    required this.selectedIcon,\n    required this.onTap,\n  });\n}\n\nclass NavigationRailDrawer extends StatelessWidget {\n  const NavigationRailDrawer({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DrawerCubit, DrawerState>(\n      builder: (BuildContext context, DrawerState drawerState) {\n        return BlocBuilder<FilterListBloc, FilterListState>(\n          builder: (BuildContext context, FilterListState filterListState) {\n            List<DrawerDestination> destinations = <DrawerDestination>[\n              DrawerDestination(\n                label: 'Todos',\n                icon: const Icon(Icons.checklist_outlined),\n                selectedIcon: const Icon(Icons.checklist),\n                onTap: (BuildContext context) => context.goNamed('todo-list'),\n              ),\n              DrawerDestination(\n                label: 'Filters',\n                icon: const Icon(Icons.filter_list_outlined),\n                selectedIcon: const Icon(Icons.filter_list),\n                onTap: (BuildContext context) => context.goNamed('filter-list'),\n              ),\n              for (Filter filter in filterListState.filterList)\n                DrawerDestination(\n                  label: 'Filter: ${filter.name}',\n                  icon: const Icon(Icons.star_outline),\n                  selectedIcon: const Icon(Icons.star),\n                  onTap: (BuildContext context) =>\n                      context.goNamed('todo-list', extra: filter),\n                ),\n              DrawerDestination(\n                label: 'Settings',\n                icon: const Icon(Icons.settings_outlined),\n                selectedIcon: const Icon(Icons.settings),\n                onTap: (BuildContext context) => context.goNamed('settings'),\n              ),\n            ];\n\n            return NavigationRail(\n              extended: true,\n              selectedIndex: drawerState.index,\n              groupAlignment: -1,\n              onDestinationSelected: (int index) {\n                final DrawerDestination d = destinations[index];\n                // Navigate if location will be changed only.\n                if (drawerState.index != index) {\n                  context.read<DrawerCubit>().next(index);\n                  d.onTap(context);\n                }\n              },\n              labelType: NavigationRailLabelType.none,\n              destinations: <NavigationRailDestination>[\n                for (DrawerDestination d in destinations)\n                  NavigationRailDestination(\n                    label: Text(\n                      d.label,\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    icon: d.icon,\n                    selectedIcon: d.selectedIcon,\n                  ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass BottomSheetNavigationDrawer extends StatelessWidget {\n  const BottomSheetNavigationDrawer({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return DraggableScrollableSheet(\n      initialChildSize: 0.35,\n      minChildSize: 0.15,\n      maxChildSize: 0.6,\n      expand: false,\n      builder: (BuildContext context, ScrollController scrollController) {\n        return BlocBuilder<DrawerCubit, DrawerState>(\n          builder: (BuildContext context, DrawerState drawerState) {\n            return BlocBuilder<FilterListBloc, FilterListState>(\n              builder: (BuildContext context, FilterListState filterListState) {\n                List<DrawerDestination> destinations = <DrawerDestination>[\n                  DrawerDestination(\n                    label: 'Todos',\n                    icon: const Icon(Icons.checklist_outlined),\n                    selectedIcon: const Icon(Icons.checklist),\n                    onTap: (BuildContext context) =>\n                        context.goNamed('todo-list'),\n                  ),\n                  DrawerDestination(\n                    label: 'Filters',\n                    icon: const Icon(Icons.filter_list_outlined),\n                    selectedIcon: const Icon(Icons.filter_list),\n                    onTap: (BuildContext context) =>\n                        context.goNamed('filter-list'),\n                  ),\n                  for (Filter filter in filterListState.filterList)\n                    DrawerDestination(\n                      label: 'Filter: ${filter.name}',\n                      icon: const Icon(Icons.star_outline),\n                      selectedIcon: const Icon(Icons.star),\n                      onTap: (BuildContext context) =>\n                          context.goNamed('todo-list', extra: filter),\n                    ),\n                  DrawerDestination(\n                    label: 'Settings',\n                    icon: const Icon(Icons.settings_outlined),\n                    selectedIcon: const Icon(Icons.settings),\n                    onTap: (BuildContext context) =>\n                        context.pushNamed('settings'),\n                  ),\n                ];\n\n                return ScrollConfiguration(\n                  behavior: CustomScrollBehavior(),\n                  child: ListView.builder(\n                    controller: scrollController,\n                    itemCount: destinations.length,\n                    itemBuilder: (BuildContext context, int index) {\n                      final DrawerDestination d = destinations[index];\n                      return Column(\n                        children: [\n                          if (index == 0) const SizedBox(height: 14.0),\n                          Padding(\n                            padding: const EdgeInsets.symmetric(\n                                horizontal: 16.0, vertical: 2.0),\n                            child: ListTile(\n                              selected: drawerState.index == index,\n                              leading: drawerState.index == index\n                                  ? d.selectedIcon\n                                  : d.icon,\n                              title: Text(\n                                d.label,\n                                maxLines: 1,\n                                overflow: TextOverflow.ellipsis,\n                              ),\n                              shape: const StadiumBorder(),\n                              onTap: () {\n                                // Navigate if location will be changed only.\n                                if (drawerState.index != index) {\n                                  context.read<DrawerCubit>().next(index);\n                                  d.onTap(context);\n                                }\n                                Navigator.pop(context);\n                              },\n                            ),\n                          ),\n                          if (index == destinations.length - 2 || index == 1)\n                            const Divider(),\n                        ],\n                      );\n                    },\n                  ),\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/filter/controller/fake_filter_controller.dart",
    "content": "// coverage:ignore-file\n\nimport 'package:ntodotxt/filter/controller/filter_controller.dart'\n    show FilterControllerInterface;\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\n\nclass FakeFilterController implements FilterControllerInterface {\n  static final List<Filter> filters = [];\n\n  FakeFilterController();\n\n  @override\n  Future<List<Filter>> list() async => filters;\n\n  @override\n  Future<Filter?> get(dynamic identifier) async {\n    for (Filter s in filters) {\n      if (s.id == identifier) {\n        return s;\n      }\n    }\n    return null;\n  }\n\n  @override\n  Future<int> insert(Filter model) async {\n    filters.add(model);\n    return filters.length;\n  }\n\n  @override\n  Future<int> update(Filter model) async {\n    int index = filters.indexWhere((Filter s) => s.id == model.id);\n    if (index != -1) {\n      filters[index] = model;\n      return index;\n    } else {\n      return 0;\n    }\n  }\n\n  @override\n  Future<int> delete(dynamic identifier) async {\n    int index = filters.indexWhere((Filter s) => s.id == identifier);\n    if (index != -1) {\n      filters.removeAt(index);\n      return index;\n    } else {\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/filter/controller/filter_controller.dart",
    "content": "import 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nabstract class FilterControllerInterface\n    implements ModelControllerInterface<Filter> {}\n\nclass FilterController implements FilterControllerInterface {\n  final DatabaseController controller;\n\n  FilterController(this.controller);\n\n  @override\n  Future<List<Filter>> list() async {\n    final Database db = await controller.database;\n    final List<Map<String, dynamic>> maps = await db.query(\n      'filters',\n      orderBy: 'name',\n    );\n\n    return List.generate(maps.length, (i) {\n      return Filter.fromMap(maps[i]);\n    });\n  }\n\n  @override\n  Future<Filter?> get(dynamic identifier) async {\n    Filter? model;\n    final Database db = await controller.database;\n    final List<Map> maps = await db.query(\n      'filters',\n      columns: [\n        'id',\n        'name',\n        'priorities',\n        'projects',\n        'contexts',\n        'order',\n        'filter',\n        'group',\n      ],\n      where: 'id = ?',\n      whereArgs: [identifier as int],\n    );\n\n    if (maps.isNotEmpty) {\n      model = Filter.fromMap(maps.first);\n    }\n\n    return model;\n  }\n\n  @override\n  Future<int> insert(Filter model) async {\n    final Database db = await controller.database;\n    Map<String, dynamic> modelMap = model.toMap();\n    modelMap['id'] = null; // Ignore id in insert mode.\n    final int id = await db.insert(\n      'filters',\n      modelMap,\n      conflictAlgorithm: ConflictAlgorithm.ignore,\n    );\n\n    return id;\n  }\n\n  @override\n  Future<int> update(Filter model) async {\n    final Database db = await controller.database;\n    // @todo\n    // if (model.id == null) {\n    //   throw SqfliteDatabaseException('Missing id attribute in Filter model');\n    // }\n    final int id = await db.update(\n      'filters',\n      model.toMap(),\n      // Ensure that the model has a matching id.\n      where: 'id = ?',\n      // Pass the models id as a whereArg to prevent SQL injection.\n      whereArgs: [model.id],\n    );\n\n    return id;\n  }\n\n  @override\n  Future<int> delete(dynamic identifier) async {\n    final Database db = await controller.database;\n    final int id = await db.delete(\n      'filters',\n      // Ensure that the model has a matching id.\n      where: 'id = ?',\n      // Pass the models id as a whereArg to prevent SQL injection.\n      whereArgs: [identifier],\n    );\n\n    return id;\n  }\n}\n"
  },
  {
    "path": "lib/filter/model/filter_model.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart'\n    show Priorities, Priority, Todo;\n\nenum ListOrder {\n  ascending,\n  descending,\n}\n\nenum ListFilter {\n  all,\n  completedOnly,\n  incompletedOnly,\n}\n\nenum ListGroup {\n  none,\n  upcoming,\n  priority,\n  project,\n  context,\n}\n\nextension Order on ListOrder {\n  static Set<ListOrder> get types => {\n        for (var t in ListOrder.values) t,\n      };\n\n  static ListOrder byName(String? name) {\n    if (name != null) {\n      try {\n        return ListOrder.values.byName(name);\n      } on ArgumentError {\n        // Returns ListOrder.ascending\n      }\n    }\n\n    return ListOrder.ascending;\n  }\n\n  // A negative integer if a is smaller than b,\n  // zero if a is equal to b, and\n  // a positive integer if a is greater than b.\n  int _sort<T>(T a, T b) {\n    switch (this) {\n      case ListOrder.ascending:\n        return ascending(a, b);\n      // return a.toString().compareTo(b.toString());\n      case ListOrder.descending:\n        return descending(a, b);\n      default:\n        // Default is ascending.\n        return ascending(a, b);\n    }\n  }\n\n  int ascending<T>(T a, T b) {\n    if (a == null) {\n      return 1;\n    }\n    if (b == null) {\n      return -1;\n    }\n    if (a is Todo && b is Todo) {\n      return a.description.toLowerCase().compareTo(b.description.toLowerCase());\n    } else {\n      return a.toString().compareTo(b.toString());\n    }\n  }\n\n  int descending<T>(T a, T b) {\n    if (a == null) {\n      return -1;\n    }\n    if (b == null) {\n      return 1;\n    }\n    if (a is Todo && b is Todo) {\n      return b.description.toLowerCase().compareTo(a.description.toLowerCase());\n    } else {\n      return b.toString().compareTo(a.toString());\n    }\n  }\n\n  Iterable<T> sort<T>(Iterable<T> list) => list.toList()..sort(_sort);\n}\n\nextension Filters on ListFilter {\n  static Set<ListFilter> get types => {\n        for (var f in ListFilter.values) f,\n      };\n\n  static ListFilter byName(String? name) {\n    if (name != null) {\n      try {\n        return ListFilter.values.byName(name);\n      } on ArgumentError {\n        // Returns ListFilter.all\n      }\n    }\n\n    return ListFilter.all;\n  }\n\n  bool _apply(Todo todo) {\n    switch (this) {\n      case ListFilter.all:\n        return true;\n      case ListFilter.completedOnly:\n        return todo.completion;\n      case ListFilter.incompletedOnly:\n        return !todo.completion;\n      default:\n        // Default is all.\n        return true;\n    }\n  }\n\n  Iterable<Todo> apply(Iterable<Todo> todoList) => todoList.where(_apply);\n}\n\nextension Groups on ListGroup {\n  static Set<ListGroup> get types => {\n        for (var g in ListGroup.values) g,\n      };\n\n  static ListGroup byName(String? name) {\n    if (name != null) {\n      try {\n        return ListGroup.values.byName(name);\n      } on ArgumentError {\n        // Returns ListGroup.none\n      }\n    }\n\n    return ListGroup.none;\n  }\n\n  Map<String, Iterable<Todo>> groupByNone({\n    required Iterable<Todo> todoList,\n  }) {\n    Map<String, Iterable<Todo>> groups = {'All': todoList};\n    groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections.\n\n    return groups;\n  }\n\n  Map<String, Iterable<Todo>> groupByUpcoming({\n    required Iterable<Todo> todoList,\n  }) {\n    Map<String, Iterable<Todo>> groups = {\n      'Deadline passed': todoList.where(\n        (Todo t) {\n          DateTime? due = t.dueDate;\n          return (due != null && Todo.compareToToday(due) < 0) ? true : false;\n        },\n      ),\n      'Today': todoList.where(\n        (Todo t) {\n          DateTime? due = t.dueDate;\n          return (due != null && Todo.compareToToday(due) == 0) ? true : false;\n        },\n      ),\n      'Upcoming': todoList.where(\n        (Todo t) {\n          DateTime? due = t.dueDate;\n          return (due != null && Todo.compareToToday(due) > 0) ? true : false;\n        },\n      ),\n      'No deadline': todoList.where(\n        (Todo t) => t.dueDate == null,\n      ),\n    };\n    groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections.\n\n    return groups;\n  }\n\n  Map<String, Iterable<Todo>> groupByPriority({\n    required Iterable<Todo> todoList,\n    required Set<Priority> sections,\n  }) {\n    Map<String, Iterable<Todo>> groups = {};\n    for (var p in sections) {\n      final Iterable<Todo> items = todoList.where((Todo t) => t.priority == p);\n      if (p == Priority.none) {\n        groups['No priority'] = items;\n      } else {\n        groups[p.name] = items;\n      }\n    }\n    groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections.\n\n    return groups;\n  }\n\n  Map<String, Iterable<Todo>> groupByProject({\n    required Iterable<Todo> todoList,\n    required Set<String?> sections,\n  }) {\n    Map<String, Iterable<Todo>> groups = {};\n    // Consider also todos without projects.\n    for (var p in [...sections, null]) {\n      Iterable<Todo> items;\n      if (p == null) {\n        items = todoList.where((Todo t) => t.projects.isEmpty);\n      } else {\n        items = todoList.where((Todo t) => t.projects.contains(p));\n      }\n      if (items.isNotEmpty) {\n        groups[p ?? 'No project'] = items;\n      }\n    }\n    groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections.\n\n    return groups;\n  }\n\n  Map<String, Iterable<Todo>> groupByContext({\n    required Iterable<Todo> todoList,\n    required Set<String?> sections,\n  }) {\n    Map<String, Iterable<Todo>> groups = {};\n    // Consider also todos without contexts.\n    for (var c in [...sections, null]) {\n      Iterable<Todo> items;\n      if (c == null) {\n        items = todoList.where((Todo t) => t.contexts.isEmpty);\n      } else {\n        items = todoList.where((Todo t) => t.contexts.contains(c));\n      }\n      if (items.isNotEmpty) {\n        groups[c ?? 'No context'] = items;\n      }\n    }\n    groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections.\n\n    return groups;\n  }\n}\n\nclass Filter extends Equatable {\n  final int? id;\n  final String name;\n  final Set<Priority> priorities;\n  final Set<String> projects;\n  final Set<String> contexts;\n  final ListOrder order;\n  final ListFilter filter;\n  final ListGroup group;\n\n  const Filter({\n    this.id,\n    this.name = '',\n    this.priorities = const {},\n    this.projects = const {},\n    this.contexts = const {},\n    this.order = ListOrder.ascending,\n    this.filter = ListFilter.all,\n    this.group = ListGroup.none,\n  });\n\n  factory Filter.fromMap(Map<dynamic, dynamic> map) {\n    return Filter(\n      id: map['id'] as int,\n      name: map['name'] as String,\n      priorities: {\n        for (var p in map['priorities'].split(',')..sort())\n          if (p != null && p.isNotEmpty) Priorities.byName(p)\n      },\n      projects: {\n        for (var p in map['projects'].split(',')..sort())\n          if (p != null && p.isNotEmpty) p,\n      },\n      contexts: {\n        for (var c in map['contexts'].split(',')..sort())\n          if (c != null && c.isNotEmpty) c,\n      },\n      order: Order.byName(map['order']),\n      filter: Filters.byName(map['filter']),\n      group: Groups.byName(map['group']),\n    );\n  }\n\n  static String get tableRepr {\n    return '''CREATE TABLE IF NOT EXISTS filters(\n      `id` INTEGER PRIMARY KEY,\n      `name` TEXT NOT NULL,\n      `priorities` TEXT,\n      `projects` TEXT,\n      `contexts` TEXT,\n      `order` TEXT,\n      `filter` TEXT,\n      `group` TEXT\n    )''';\n  }\n\n  Iterable<Todo> apply(List<Todo> todoList) {\n    Iterable<Todo> filtered = filter.apply(todoList);\n    if (priorities.isNotEmpty) {\n      filtered = filtered.where(_applyPriority);\n    }\n    if (projects.isNotEmpty) {\n      filtered = filtered.where(_applyProject);\n    }\n    if (contexts.isNotEmpty) {\n      filtered = filtered.where(_applyContext);\n    }\n\n    // Completed todos come always at last.\n    filtered = order.sort(filtered);\n    return (filtered.where((Todo t) => !t.completion).toList() +\n        filtered.where((Todo t) => t.completion).toList());\n  }\n\n  bool _applyPriority(Todo todo) => priorities.contains(todo.priority);\n\n  bool _applyProject(Todo todo) {\n    for (String p in projects) {\n      if (todo.projects.contains(p)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  bool _applyContext(Todo todo) {\n    for (String c in contexts) {\n      if (todo.contexts.contains(c)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  Map<String, Iterable<Todo>> grouped(Iterable<Todo> todoList) {\n    switch (group) {\n      case ListGroup.none:\n        return group.groupByNone(todoList: todoList);\n      case ListGroup.upcoming:\n        return group.groupByUpcoming(todoList: todoList);\n      case ListGroup.priority:\n        return group.groupByPriority(\n          todoList: todoList,\n          sections: order.sort(Priority.values).toSet(),\n        );\n      case ListGroup.project:\n        final Set<String> projects =\n            todoList.map((Todo todo) => todo.projects).fold<Set<String>>(\n          {},\n          (Set<String> previousValue, Set<String> value) {\n            return previousValue..addAll(value);\n          },\n        );\n        return group.groupByProject(\n          todoList: todoList,\n          sections: order.sort(projects).toSet(),\n        );\n      case ListGroup.context:\n        Set<String> contexts =\n            todoList.map((Todo todo) => todo.contexts).fold<Set<String>>(\n          {},\n          (Set<String> previousValue, Set<String> value) {\n            return previousValue..addAll(value);\n          },\n        );\n        return group.groupByContext(\n          todoList: todoList,\n          sections: order.sort(contexts).toSet(),\n        );\n      default:\n        // Default is none.\n        return group.groupByNone(todoList: todoList);\n    }\n  }\n\n  Filter copyWith({\n    int? id,\n    String? name,\n    Set<Priority>? priorities,\n    Set<String>? projects,\n    Set<String>? contexts,\n    ListOrder? order,\n    ListFilter? filter,\n    ListGroup? group,\n  }) {\n    return Filter(\n      id: id ?? this.id,\n      name: name ?? this.name,\n      priorities: Priorities.sort(priorities ?? this.priorities),\n      projects: projects != null\n          ? (projects.toList()..sort()).toSet()\n          : this.projects,\n      contexts: contexts != null\n          ? (contexts.toList()..sort()).toSet()\n          : this.contexts,\n      order: order ?? this.order,\n      filter: filter ?? this.filter,\n      group: group ?? this.group,\n    );\n  }\n\n  Filter copyWithUnsaved({\n    Set<Priority>? priorities,\n    Set<String>? projects,\n    Set<String>? contexts,\n    ListOrder? order,\n    ListFilter? filter,\n    ListGroup? group,\n  }) {\n    return Filter(\n      priorities: priorities ?? this.priorities,\n      projects: projects ?? this.projects,\n      contexts: contexts ?? this.contexts,\n      order: order ?? this.order,\n      filter: filter ?? this.filter,\n      group: group ?? this.group,\n    );\n  }\n\n  /// Convert a [Filter] into Map.\n  /// The keys must correspond to the names of the\n  /// columns in the database.\n  Map<String, dynamic> toMap() {\n    return {\n      'id': id,\n      'name': name,\n      'priorities': [for (var p in priorities) p.name].join(','),\n      'projects': projects.join(','),\n      'contexts': contexts.join(','),\n      'order': order.name,\n      'filter': filter.name,\n      'group': group.name,\n    };\n  }\n\n  @override\n  String toString() {\n    return 'Filter { id: $id, name: $name order: ${order.name} filter: ${filter.name} group: ${group.name} priorities: ${[\n      for (var p in priorities) p.name\n    ]} projects: ${[for (var p in projects) p]} contexts: ${[\n      for (var c in contexts) c\n    ]} }';\n  }\n\n  @override\n  List<Object?> get props => [\n        id,\n        name,\n        priorities,\n        projects,\n        contexts,\n        order,\n        filter,\n        group,\n      ];\n}\n"
  },
  {
    "path": "lib/filter/page/filter_create_edit_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart' show SnackBarHandler;\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\n\nclass FilterCreateEditPage extends StatelessWidget {\n  final Filter? initFilter;\n  final Set<String> projects;\n  final Set<String> contexts;\n\n  const FilterCreateEditPage({\n    this.initFilter,\n    this.projects = const {},\n    this.contexts = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => FilterCubit(\n        settingRepository: context.read<SettingRepository>(),\n        filterRepository: context.read<FilterRepository>(),\n        filter: initFilter,\n      ),\n      child: GestureDetector(\n        onTap: () {\n          FocusScopeNode currentFocus = FocusScope.of(context);\n          if (!currentFocus.hasPrimaryFocus) {\n            currentFocus.unfocus();\n          }\n        },\n        child: FilterDialogWrapper(\n          newFilter: initFilter == null,\n          child: Scaffold(\n            appBar: MainAppBar(\n              title: initFilter == null ? 'Create' : 'Edit',\n              toolbar: Row(\n                children: <Widget>[\n                  if (initFilter != null) const DeleteFilterIconButton(),\n                  SaveFilterIconButton(initFilter: initFilter),\n                ],\n              ),\n            ),\n            body: ListView(\n              children: [\n                const FilterNameTextField(),\n                const Divider(),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: ListTile(\n                    title: Text(\n                      'General',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                ),\n                const FilterOrderItem(),\n                const FilterFilterItem(),\n                const FilterGroupItem(),\n                const Divider(),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: ListTile(\n                    title: Text(\n                      'Tags',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                ),\n                const FilterPrioritiesItem(),\n                FilterProjectTagsItem(availableTags: projects),\n                FilterContextTagsItem(availableTags: contexts),\n                const SizedBox(height: 16),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass FilterDialogWrapper extends StatelessWidget {\n  final Widget child;\n  final bool newFilter;\n\n  const FilterDialogWrapper({\n    required this.child,\n    required this.newFilter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        return PopScope(\n          canPop: false,\n          onPopInvokedWithResult: <T>(bool didPop, T? result) async {\n            if (didPop) {\n              return;\n            }\n            if (state.filter.name.isEmpty) {\n              if (!await ConfirmationDialog.dialog(\n                context: context,\n                title: newFilter ? 'Create filter' : 'Edit filter',\n                message: 'Cannot save a filter with an empty name.',\n                cancelLabel: 'Cancel',\n                actionLabel: 'Continue',\n              )) {\n                if (context.mounted) {\n                  context.pop();\n                }\n              }\n            } else {\n              if (state.changed) {\n                final bool confirm = await ConfirmationDialog.dialog(\n                  context: context,\n                  title: 'Save filter',\n                  message:\n                      'Filter contains unsaved changes. These will be irrecoverably lost.',\n                  actionLabel: 'Save',\n                  cancelLabel: 'Discard',\n                );\n                if (context.mounted && confirm) {\n                  if (newFilter) {\n                    await context.read<FilterCubit>().create(state.filter);\n                    if (context.mounted) {\n                      SnackBarHandler.info(context, 'Filter has been created');\n                    }\n                  } else {\n                    await context.read<FilterCubit>().update(state.filter);\n                    if (context.mounted) {\n                      SnackBarHandler.info(context, 'Filter has been updated');\n                    }\n                  }\n                }\n              }\n              if (context.mounted) {\n                context.pop();\n              }\n            }\n          },\n          child: child,\n        );\n      },\n    );\n  }\n}\n\nclass SaveFilterIconButton extends StatelessWidget {\n  final Filter? initFilter;\n  const SaveFilterIconButton({\n    required this.initFilter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        return Visibility(\n          visible: state.changed && state.filter.name.isNotEmpty,\n          child: IconButton(\n            tooltip: 'Save',\n            icon: const Icon(Icons.save),\n            onPressed: () async {\n              if (initFilter == null) {\n                await context.read<FilterCubit>().create(state.filter);\n              } else {\n                await context.read<FilterCubit>().update(state.filter);\n              }\n              if (context.mounted) {\n                SnackBarHandler.info(context, 'Filter saved');\n                context.pop();\n              }\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DeleteFilterIconButton extends StatelessWidget {\n  const DeleteFilterIconButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        return IconButton(\n          tooltip: 'Delete',\n          icon: const Icon(Icons.delete),\n          onPressed: () async {\n            final bool confirm = await ConfirmationDialog.dialog(\n              context: context,\n              title: 'Delete filter',\n              message: 'Do you want to delete the filter?',\n              actionLabel: 'Delete',\n              cancelLabel: 'Cancel',\n            );\n            if (context.mounted && confirm) {\n              await context.read<FilterCubit>().delete(state.filter);\n              if (context.mounted) {\n                SnackBarHandler.info(context, 'Filter has been deleted');\n                context.pop();\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterNameTextField extends StatefulWidget {\n  const FilterNameTextField({super.key});\n\n  @override\n  State<FilterNameTextField> createState() => _FilterNameTextFieldState();\n}\n\nclass _FilterNameTextFieldState extends State<FilterNameTextField> {\n  late GlobalKey<FormFieldState> _textFormKey;\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _textFormKey = GlobalKey<FormFieldState>();\n    _controller = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    // Clean up the controller when the widget is disposed.\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.name != state.filter.name;\n      },\n      builder: (BuildContext context, FilterState state) {\n        // Setting text and selection together.\n        int base = _controller.selection.base.offset;\n        _controller.value = _controller.value.copyWith(\n          text: state.filter.name,\n          selection: TextSelection.fromPosition(\n            TextPosition(\n              offset: base < 0 || base > state.filter.name.length\n                  ? state.filter.name.length\n                  : base,\n            ),\n          ),\n        );\n        return TextFormField(\n          key: _textFormKey,\n          controller: _controller,\n          minLines: 1,\n          maxLines: 1,\n          keyboardType: TextInputType.text,\n          inputFormatters: [\n            FilteringTextInputFormatter.deny(RegExp(r'\\n')),\n          ],\n          style: Theme.of(context).textTheme.titleMedium,\n          textCapitalization: TextCapitalization.sentences,\n          decoration: const InputDecoration(\n            hintText: 'Filter name',\n            contentPadding: EdgeInsets.symmetric(\n              horizontal: 20.0,\n              vertical: 16.0,\n            ),\n          ),\n          onChanged: (String value) =>\n              context.read<FilterCubit>().updateName(value),\n        );\n      },\n    );\n  }\n}\n\nclass FilterOrderItem extends StatelessWidget {\n  const FilterOrderItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.order != state.filter.order;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.sort),\n            title: const Text('Order'),\n            subtitle: GenericChipGroup(\n              children: [\n                BasicChip(label: state.filter.order.name),\n              ],\n            ),\n            onTap: () async {\n              await FilterStateOrderDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterFilterItem extends StatelessWidget {\n  const FilterFilterItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.filter != state.filter.filter;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.filter_list),\n            title: const Text('Filter'),\n            subtitle: GenericChipGroup(\n              children: [\n                BasicChip(label: state.filter.filter.name),\n              ],\n            ),\n            onTap: () async {\n              await FilterStateFilterDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterGroupItem extends StatelessWidget {\n  const FilterGroupItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.group != state.filter.group;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.workspaces_outlined),\n            title: const Text('Group by'),\n            subtitle: GenericChipGroup(\n              children: [\n                BasicChip(\n                  label: state.filter.group.name,\n                ),\n              ],\n            ),\n            onTap: () async {\n              await FilterStateGroupDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterPrioritiesItem extends StatelessWidget {\n  const FilterPrioritiesItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.priorities != state.filter.priorities;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.flag_outlined),\n            title: const Text('Priorities'),\n            subtitle: state.filter.priorities.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.filter.priorities)\n                        BasicChip(label: t.name),\n                    ],\n                  ),\n            onTap: () async {\n              await FilterPriorityTagDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n                availableTags: Priority.values.toSet(),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterProjectTagsItem extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const FilterProjectTagsItem({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.projects != state.filter.projects;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.rocket_launch_outlined),\n            title: const Text('Projects'),\n            subtitle: state.filter.projects.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.filter.projects) BasicChip(label: t),\n                    ],\n                  ),\n            onTap: () async {\n              await FilterProjectTagDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n                availableTags: availableTags,\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FilterContextTagsItem extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const FilterContextTagsItem({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) {\n        return previousState.filter.contexts != state.filter.contexts;\n      },\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            leading: const Icon(Icons.join_inner),\n            title: const Text('Contexts'),\n            subtitle: state.filter.contexts.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.filter.contexts) BasicChip(label: t),\n                    ],\n                  ),\n            onTap: () async {\n              await FilterContextTagDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n                availableTags: availableTags,\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/filter/page/filter_list_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart' show PopScopeDrawer, SnackBarHandler;\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\nimport 'package:ntodotxt/common/widget/scroll_to_top.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/filter/state/filter_list_state.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_state.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\n\nclass FilterListPage extends StatelessWidget {\n  const FilterListPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    // @todo: Activate WideLayout later!\n    return const FilterListViewNarrow();\n    // final bool isNarrowLayout =\n    //     MediaQuery.of(context).size.width < maxScreenWidthCompact;\n    // return isNarrowLayout\n    //     ? const FilterListViewNarrow()\n    //     : const FilterListViewWide();\n  }\n}\n\n///\n/// Narrow layout\n///\n\nclass FilterListViewNarrow extends ScollToTopView {\n  const FilterListViewNarrow({super.key});\n\n  @override\n  State<FilterListViewNarrow> createState() => _FilterListViewNarrowState();\n}\n\nclass _FilterListViewNarrowState\n    extends ScollToTopViewState<FilterListViewNarrow> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterListBloc, FilterListState>(\n      builder: (BuildContext context, FilterListState state) {\n        return PopScopeDrawer(\n          child: Scaffold(\n            appBar: const MainAppBar(title: 'Filters'),\n            body: ListView.builder(\n              controller: scrollController,\n              padding: const EdgeInsets.symmetric(horizontal: 8.0),\n              itemCount: state.filterList.length,\n              itemBuilder: (BuildContext context, int index) {\n                return FilterListTile(filter: state.filterList[index]);\n              },\n            ),\n            drawer: Container(),\n            floatingActionButton: scrolledDown\n                ? FloatingActionButton.small(\n                    tooltip: 'Go to top',\n                    child: const Icon(Icons.keyboard_arrow_up),\n                    onPressed: () => scrollToTop(),\n                  )\n                : FloatingActionButton(\n                    tooltip: 'Add filter',\n                    child: const Icon(Icons.add),\n                    onPressed: () => context.pushNamed('filter-create'),\n                  ),\n          ),\n        );\n      },\n    );\n  }\n}\n\n///\n/// Wide layout\n///\n\nclass FilterListViewWide extends ScollToTopView {\n  const FilterListViewWide({super.key});\n\n  @override\n  State<FilterListViewWide> createState() => _FilterListViewWideState();\n}\n\nclass _FilterListViewWideState extends ScollToTopViewState<FilterListViewWide> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterListBloc, FilterListState>(\n      builder: (BuildContext context, FilterListState state) {\n        return PopScopeDrawer(\n          child: Scaffold(\n            appBar: const MainAppBar(title: 'Filters'),\n            body: ListView.builder(\n              controller: scrollController,\n              padding: const EdgeInsets.symmetric(horizontal: 8.0),\n              itemCount: state.filterList.length,\n              itemBuilder: (BuildContext context, int index) {\n                return FilterListTile(filter: state.filterList[index]);\n              },\n            ),\n            floatingActionButton: scrolledDown\n                ? FloatingActionButton.small(\n                    tooltip: 'Go to top',\n                    child: const Icon(Icons.keyboard_arrow_up),\n                    onPressed: () => scrollToTop(),\n                  )\n                : FloatingActionButton(\n                    tooltip: 'Add filter',\n                    child: const Icon(Icons.add),\n                    onPressed: () => context.pushNamed('filter-create'),\n                  ),\n          ),\n        );\n      },\n    );\n  }\n}\n\n///\n/// Components\n///\n\nclass FilterListTile extends StatelessWidget {\n  final Filter filter;\n\n  const FilterListTile({\n    required this.filter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<InteractionSettingsCubit, InteractionSettingsState>(\n      buildWhen: (InteractionSettingsState previousState,\n              InteractionSettingsState state) =>\n          previousState.swipeRightActionEnabled !=\n          state.swipeRightActionEnabled,\n      builder: (BuildContext context, InteractionSettingsState state) {\n        return Dismissible(\n          key: ValueKey<int>(filter.id!),\n          direction: state.swipeRightActionEnabled\n              ? DismissDirection.startToEnd\n              : DismissDirection.none,\n          dismissThresholds: const {\n            DismissDirection.startToEnd: 0.5,\n            DismissDirection.endToStart: 0.5,\n          },\n          background: Container(\n            color: Theme.of(context).colorScheme.error, // red\n            alignment: Alignment.centerLeft,\n            padding: const EdgeInsets.only(left: 16.0),\n            child: Icon(Icons.delete),\n          ),\n          confirmDismiss: (DismissDirection direction) async {\n            // Delete\n            if (direction == DismissDirection.startToEnd) {\n              return await ConfirmationDialog.dialog(\n                context: context,\n                title: 'Delete filter',\n                message: 'Do you want to delete the filter?',\n                actionLabel: 'Delete',\n                cancelLabel: 'Cancel',\n              );\n            }\n            return true;\n          },\n          onDismissed: (DismissDirection direction) async {\n            if (direction == DismissDirection.startToEnd) {\n              // Delete\n              context.read<FilterListBloc>().add(\n                    FilterListFilterDeleted(filter: filter),\n                  );\n              SnackBarHandler.info(context, 'Filter has been deleted');\n            }\n          },\n          child: ListTile(\n            key: ValueKey<int>(filter.id!),\n            title: Text(\n              filter.name,\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n            ),\n            subtitle: _buildSubtitle(),\n            onTap: () => context.pushNamed('filter-edit', extra: filter),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget? _buildSubtitle() {\n    final List<String> items = [\n      filter.order.name,\n      filter.filter.name,\n      filter.group.name,\n      for (Priority p in filter.priorities) p.name,\n      for (String p in filter.projects) '+$p',\n      for (String c in filter.contexts) '@$c',\n    ]..removeWhere((value) => value.isEmpty);\n\n    if (items.isEmpty) {\n      return null;\n    }\n\n    List<String> shortenedItems;\n    if (items.length > 5) {\n      shortenedItems = items.sublist(0, 5);\n      shortenedItems.add('...');\n    } else {\n      shortenedItems = [...items];\n    }\n\n    return Wrap(\n      crossAxisAlignment: WrapCrossAlignment.center,\n      spacing: 4.0, // gap between adjacent chips\n      runSpacing: 4.0, // gap between lines\n      children: <Widget>[\n        for (String attr in shortenedItems) BasicChip(label: attr, mono: true),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/filter/repository/filter_repository.dart",
    "content": "import 'package:ntodotxt/filter/controller/filter_controller.dart'\n    show FilterController;\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:rxdart/rxdart.dart';\n\nclass FilterRepository {\n  final FilterController controller;\n  final BehaviorSubject<List<Filter>> _streamController =\n      BehaviorSubject<List<Filter>>.seeded(const []);\n\n  FilterRepository(this.controller);\n\n  Stream<List<Filter>> get stream => _streamController.asBroadcastStream();\n\n  Future<void> refresh() async {\n    _streamController.sink.add(await controller.list());\n  }\n\n  void dispose() {\n    _streamController.close();\n  }\n\n  Future<List<Filter>> list() async => await controller.list();\n\n  Future<Filter?> get({required int id}) async => await controller.get(id);\n\n  Future<int> insert(Filter model) async {\n    final int result = await controller.insert(model);\n    await refresh();\n    return result;\n  }\n\n  Future<int> update(Filter model) async {\n    final int result = await controller.update(model);\n    await refresh();\n    return result;\n  }\n\n  Future<int> delete({required int id}) async {\n    final int result = await controller.delete(id);\n    await refresh();\n    return result;\n  }\n}\n"
  },
  {
    "path": "lib/filter/state/filter_cubit.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show Filter, Filters, Groups, ListFilter, ListGroup, ListOrder, Order;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart'\n    show FilterRepository;\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/model/setting_model.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\n\nclass FilterCubit extends Cubit<FilterState> {\n  final SettingRepository _settingRepository;\n  final FilterRepository _filterRepository;\n\n  FilterCubit({\n    required SettingRepository settingRepository,\n    required FilterRepository filterRepository,\n    Filter? filter,\n  })  : _settingRepository = settingRepository,\n        _filterRepository = filterRepository,\n        super(\n          filter == null\n              ? FilterLoading(filter: const Filter())\n              : FilterSaved(filter: filter),\n        );\n\n  Future<void> load() async {\n    try {\n      if (state is FilterLoading) {\n        emit(\n          state.save(\n            filter: Filter(\n              order: Order.byName(\n                  (await _settingRepository.get(key: 'order'))?.value),\n              filter: Filters.byName(\n                  (await _settingRepository.get(key: 'filter'))?.value),\n              group: Groups.byName(\n                  (await _settingRepository.get(key: 'group'))?.value),\n            ),\n          ),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  ///\n  /// Regular filter (saved filter)\n  ///\n\n  Future<void> create(Filter f) async {\n    try {\n      final Filter filter = f.copyWith(name: f.name.trim());\n      int id = await _filterRepository.insert(filter);\n      if (id > 0) {\n        emit(state.save(filter: filter.copyWith(id: id)));\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> update(Filter f) async {\n    try {\n      final Filter filter = f.copyWith(name: f.name.trim());\n      int id = await _filterRepository.update(filter);\n      if (id > 0) {\n        emit(state.save(filter: filter));\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> delete(Filter filter) async {\n    try {\n      if (filter.id != null) {\n        await _filterRepository.delete(id: filter.id!);\n      }\n      emit(state.save(filter: filter.copyWithUnsaved()));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateName(String name) {\n    try {\n      emit(state.update(\n        filter: state.filter.copyWith(\n          name: name.replaceAllMapped(RegExp(r'\\s{2,}'), (match) => ' '),\n        ),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateOrder(ListOrder order) {\n    try {\n      emit(state.update(\n        filter: state.filter.copyWith(order: order),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateFilter(ListFilter filter) {\n    try {\n      emit(state.update(\n        filter: state.filter.copyWith(filter: filter),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateGroup(ListGroup group) {\n    try {\n      emit(state.update(\n        filter: state.filter.copyWith(group: group),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addPriority(Priority priority) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            priorities: {...state.filter.priorities, priority},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removePriority(Priority priority) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            priorities: {...state.filter.priorities}..remove(priority),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updatePriorities(Set<Priority> priorities) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            priorities: {...priorities},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addProject(String project) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            projects: {...state.filter.projects, project},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removeProject(String project) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            projects: {...state.filter.projects}..remove(project),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateProjects(Set<String> projects) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            projects: {...projects},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addContext(String context) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            contexts: {...state.filter.contexts, context},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removeContext(String context) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            contexts: {...state.filter.contexts}..remove(context),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateContexts(Set<String> contexts) {\n    try {\n      emit(\n        state.update(\n          filter: state.filter.copyWith(\n            contexts: {...contexts},\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  ///\n  /// Default filter\n  ///\n\n  Future<void> resetToDefaults() async {\n    try {\n      const Filter defaultFilter = Filter();\n      for (var k in ['order', 'filter', 'group']) {\n        await _settingRepository.delete(key: k);\n      }\n      emit(state.save(filter: defaultFilter));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> updateDefaultOrder(ListOrder? value) async {\n    try {\n      if (value != null) {\n        emit(\n          state.save(\n            filter: state.filter.copyWith(order: value),\n          ),\n        );\n        await _settingRepository.updateOrInsert(\n          Setting(key: 'order', value: value.name),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> updateDefaultFilter(ListFilter? value) async {\n    try {\n      if (value != null) {\n        emit(\n          state.save(\n            filter: state.filter.copyWith(filter: value),\n          ),\n        );\n        await _settingRepository.updateOrInsert(\n          Setting(key: 'filter', value: value.name),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> updateDefaultGroup(ListGroup? value) async {\n    try {\n      if (value != null) {\n        emit(\n          state.save(\n            filter: state.filter.copyWith(group: value),\n          ),\n        );\n        await _settingRepository.updateOrInsert(\n          Setting(key: 'group', value: value.name),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/filter/state/filter_list_bloc.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/filter/state/filter_list_state.dart';\n\nclass FilterListBloc extends Bloc<FilterListEvent, FilterListState> {\n  final FilterRepository _repository;\n\n  FilterListBloc({required FilterRepository repository})\n      : _repository = repository,\n        super(const FilterListLoading()) {\n    on<FilterListSubscriped>(_onFilterListSubscriped);\n    on<FilterListSynchronizationRequested>(_onFilterSynchronizationRequested);\n    on<FilterListFilterDeleted>(_onFilterDeleted);\n  }\n\n  Future<void> _onFilterListSubscriped(\n    FilterListSubscriped event,\n    Emitter<FilterListState> emit,\n  ) async {\n    await emit.forEach<List<Filter>>(\n      _repository.stream,\n      onData: (filterList) {\n        return state.copyWith(filterList: filterList);\n      },\n      onError: (e, _) => state.error(message: e.toString()),\n    );\n  }\n\n  Future<void> _onFilterSynchronizationRequested(\n    FilterListSynchronizationRequested event,\n    Emitter<FilterListState> emit,\n  ) async {\n    try {\n      emit(state.loading());\n      await _repository.refresh();\n      emit(state.success());\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> _onFilterDeleted(\n    FilterListFilterDeleted event,\n    Emitter<FilterListState> emit,\n  ) async {\n    final List<Filter> previousList = state.filterList;\n    final List<Filter> updatedList = previousList\n        .where((Filter item) => item.id != event.filter.id)\n        .toList(growable: false);\n\n    // Important: Remove item instantily for Dismissible.\n    // Otherwise we get an error:\n    // A dismissed Dismissible widget is still part of the tree.\n    emit(state.success(filterList: updatedList));\n    try {\n      await _repository.delete(id: event.filter.id!);\n    } on Exception catch (e) {\n      // If error rollback to previous list.\n      emit(state.error(message: e.toString(), filterList: previousList));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/filter/state/filter_list_event.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\n\nsealed class FilterListEvent extends Equatable {\n  const FilterListEvent();\n\n  @override\n  List<Object?> get props => [];\n}\n\nfinal class FilterListSubscriped extends FilterListEvent {\n  const FilterListSubscriped();\n}\n\nfinal class FilterListSynchronizationRequested extends FilterListEvent {\n  const FilterListSynchronizationRequested();\n}\n\nfinal class FilterListFilterDeleted extends FilterListEvent {\n  final Filter filter;\n\n  const FilterListFilterDeleted({\n    required this.filter,\n  });\n\n  @override\n  List<Object?> get props => [filter];\n}\n"
  },
  {
    "path": "lib/filter/state/filter_list_state.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\n\nsealed class FilterListState extends Equatable {\n  final List<Filter> filterList;\n\n  const FilterListState({\n    this.filterList = const [],\n  });\n\n  FilterListState copyWith({\n    List<Filter>? filterList,\n  });\n\n  FilterListLoading loading({\n    List<Filter>? filterList,\n  }) {\n    return FilterListLoading(\n      filterList: filterList ?? this.filterList,\n    );\n  }\n\n  FilterListSuccess success({\n    List<Filter>? filterList,\n  }) {\n    return FilterListSuccess(\n      filterList: filterList ?? this.filterList,\n    );\n  }\n\n  FilterListError error({\n    required String message,\n    List<Filter>? filterList,\n  }) {\n    return FilterListError(\n      message: message,\n      filterList: filterList ?? this.filterList,\n    );\n  }\n\n  @override\n  List<Object> get props => [\n        filterList,\n      ];\n\n  @override\n  String toString() => 'FilterListState { filters: $filterList }';\n}\n\nfinal class FilterListLoading extends FilterListState {\n  const FilterListLoading({\n    super.filterList,\n  });\n\n  @override\n  FilterListLoading copyWith({List<Filter>? filterList}) =>\n      super.loading(filterList: filterList ?? this.filterList);\n\n  @override\n  String toString() => 'FilterListLoading { filters: $filterList }';\n}\n\nfinal class FilterListSuccess extends FilterListState {\n  const FilterListSuccess({\n    super.filterList,\n  });\n\n  @override\n  FilterListSuccess copyWith({List<Filter>? filterList}) =>\n      super.success(filterList: filterList ?? this.filterList);\n\n  @override\n  String toString() => 'FilterListSuccess { filters: $filterList }';\n}\n\nfinal class FilterListError extends FilterListState {\n  final String message;\n\n  const FilterListError({\n    required this.message,\n    super.filterList,\n  });\n\n  @override\n  FilterListError copyWith({\n    String? message,\n    List<Filter>? filterList,\n  }) =>\n      super.error(\n        message: message ?? this.message,\n        filterList: filterList ?? this.filterList,\n      );\n\n  @override\n  List<Object> get props => [\n        message,\n        filterList,\n      ];\n\n  @override\n  String toString() =>\n      'FilterListError { message: $message filters: $filterList }';\n}\n"
  },
  {
    "path": "lib/filter/state/filter_state.dart",
    "content": "import 'package:collection/collection.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\n\nsealed class FilterState extends Equatable {\n  final Filter filter;\n  final Filter? origin;\n\n  FilterState({\n    required this.filter,\n    Filter? origin,\n  }) : origin = origin ?? filter.copyWith();\n\n  FilterLoading loading({\n    Filter? filter,\n  }) {\n    return FilterLoading(\n      filter: filter ?? this.filter,\n      origin: origin,\n    );\n  }\n\n  FilterChanged update({\n    Filter? filter,\n  }) {\n    return FilterChanged(\n      filter: filter ?? this.filter,\n      origin: origin,\n    );\n  }\n\n  FilterSaved save({\n    Filter? filter,\n  }) {\n    return FilterSaved(\n      filter: filter ?? this.filter,\n    );\n  }\n\n  FilterError error({\n    required String message,\n    Filter? filter,\n  }) {\n    return FilterError(\n      message: message,\n      filter: filter ?? this.filter,\n      origin: origin,\n    );\n  }\n\n  bool get changed => origin != filter;\n\n  bool get orderChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return origin!.order != filter.order;\n    }\n  }\n\n  bool get filterChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return origin!.filter != filter.filter;\n    }\n  }\n\n  bool get groupChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return origin!.group != filter.group;\n    }\n  }\n\n  bool get prioritiesChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return !const SetEquality().equals(origin!.priorities, filter.priorities);\n    }\n  }\n\n  bool get projectsChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return !const SetEquality().equals(origin!.projects, filter.projects);\n    }\n  }\n\n  bool get contextsChanged {\n    if (origin == null) {\n      return true;\n    } else {\n      return !const SetEquality().equals(origin!.contexts, filter.contexts);\n    }\n  }\n\n  @override\n  List<Object?> get props => [\n        filter,\n        origin,\n      ];\n\n  @override\n  String toString() => 'FilterState { filter: $filter }';\n}\n\nfinal class FilterLoading extends FilterState {\n  FilterLoading({\n    required super.filter,\n    super.origin,\n  });\n\n  FilterLoading copyWith({\n    Filter? filter,\n  }) =>\n      super.loading(filter: filter ?? this.filter);\n\n  @override\n  String toString() => 'FilterLoading { filter: $filter }';\n}\n\nfinal class FilterChanged extends FilterState {\n  FilterChanged({\n    required super.filter,\n    super.origin,\n  });\n\n  FilterChanged copyWith({\n    Filter? filter,\n  }) =>\n      super.update(filter: filter ?? this.filter);\n\n  @override\n  String toString() => 'FilterChanged { filter: $filter }';\n}\n\nfinal class FilterSaved extends FilterState {\n  FilterSaved({\n    required super.filter,\n    super.origin,\n  });\n\n  FilterSaved copyWith({\n    Filter? filter,\n  }) =>\n      super.save(filter: filter ?? this.filter);\n\n  @override\n  String toString() => 'FilterSaved { filter: $filter }';\n}\n\nfinal class FilterError extends FilterState {\n  final String message;\n\n  FilterError({\n    required this.message,\n    required super.filter,\n    super.origin,\n  });\n\n  FilterError copyWith({\n    String? message,\n    Filter? filter,\n  }) =>\n      super.error(\n          message: message ?? this.message, filter: filter ?? this.filter);\n\n  @override\n  List<Object?> get props => [\n        message,\n        filter,\n        origin,\n      ];\n\n  @override\n  String toString() => 'FilterError { message: $message filter: $filter }';\n}\n"
  },
  {
    "path": "lib/filter/widget/filter_chip.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show ListFilter, ListOrder;\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\n\nclass FilterOrderChip extends StatelessWidget {\n  const FilterOrderChip({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.orderChanged;\n        return GenericActionChip(\n          avatar: const Icon(Icons.sort_by_alpha),\n          label: Row(\n            children: <Widget>[\n              Text(state.filter.order == ListOrder.ascending ? 'asc' : 'desc'),\n              const Text(' '),\n              Icon(\n                state.filter.order == ListOrder.ascending\n                    ? Icons.keyboard_arrow_up\n                    : Icons.keyboard_arrow_down,\n                size: 18.0,\n              ),\n            ],\n          ),\n          selected: changed,\n          onPressed: () async {\n            await FilterStateOrderDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterFilterChip extends StatelessWidget {\n  const FilterFilterChip({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.filterChanged;\n        late final String label;\n        late final IconData iconData;\n        switch (state.filter.filter) {\n          case ListFilter.all:\n            label = 'all';\n            iconData = Icons.filter_list;\n            break;\n          case ListFilter.completedOnly:\n            label = 'completed';\n            iconData = Icons.done_all;\n            break;\n          case ListFilter.incompletedOnly:\n            label = 'incompleted';\n            iconData = Icons.remove_done;\n            break;\n        }\n\n        return GenericActionChip(\n          avatar: Icon(iconData),\n          label: Text(label),\n          selected: changed,\n          onPressed: () async {\n            await FilterStateFilterDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterGroupChip extends StatelessWidget {\n  const FilterGroupChip({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.groupChanged;\n        return GenericActionChip(\n          avatar: Icon(changed ? Icons.workspaces : Icons.workspaces_outlined),\n          label: Text(state.filter.group.name),\n          selected: changed,\n          onPressed: () async {\n            await FilterStateGroupDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterPrioritiesChip extends StatelessWidget {\n  const FilterPrioritiesChip({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.prioritiesChanged;\n        return GenericActionChip(\n          avatar: Icon(changed ? Icons.flag : Icons.flag_outlined),\n          label: const Text('priorities'),\n          selected: changed,\n          onPressed: () async {\n            await FilterPriorityTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n              availableTags: Priority.values.toSet(),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterProjectsChip extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const FilterProjectsChip({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.projectsChanged;\n        return GenericActionChip(\n          avatar: Icon(\n              changed ? Icons.rocket_launch : Icons.rocket_launch_outlined),\n          label: const Text('projects'),\n          selected: changed,\n          onPressed: () async {\n            await FilterProjectTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n              availableTags: {...availableTags, ...state.filter.projects},\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FilterContextsChip extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const FilterContextsChip({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        final bool changed = state.contextsChanged;\n        return GenericActionChip(\n          avatar: const Icon(Icons.tag),\n          label: const Text('contexts'),\n          selected: changed,\n          onPressed: () async {\n            await FilterContextTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<FilterCubit>(context),\n              availableTags: {...availableTags, ...state.filter.contexts},\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/intro/page/intro_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/login/page/login_page.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass IntroPage extends StatefulWidget {\n  const IntroPage({super.key});\n\n  @override\n  State<IntroPage> createState() => _IntroPageState();\n}\n\nclass _IntroPageState extends State<IntroPage> {\n  int _currentPage = 0;\n\n  final PageController _pageController = PageController(initialPage: 0);\n\n  final List<Widget> pages = [\n    const IntroPageWelcome(),\n    IntroPageLocal(),\n    IntroPageWebDav(),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: PageView(\n        controller: _pageController,\n        onPageChanged: (int page) {\n          setState(() {\n            _currentPage = page;\n          });\n        },\n        children: pages,\n      ),\n      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,\n      floatingActionButton: Stack(\n        fit: StackFit.expand,\n        children: [\n          Positioned(\n            left: 30,\n            bottom: 20,\n            child: IconButton(\n              tooltip: 'Previous page',\n              icon: const Icon(\n                Icons.navigate_before,\n                size: 40,\n              ),\n              onPressed: _currentPage > 0\n                  ? () {\n                      setState(() {\n                        _currentPage -= 1;\n                      });\n                      _pageController.previousPage(\n                        duration: const Duration(milliseconds: 500),\n                        curve: Curves.ease,\n                      );\n                    }\n                  : null,\n            ),\n          ),\n          Positioned(\n            bottom: 20,\n            right: 30,\n            child: IconButton(\n              tooltip: 'Next page',\n              icon: const Icon(\n                Icons.navigate_next,\n                size: 40,\n              ),\n              onPressed: _currentPage < pages.length - 1\n                  ? () {\n                      setState(() {\n                        _currentPage += 1;\n                      });\n                      _pageController.nextPage(\n                        duration: const Duration(milliseconds: 500),\n                        curve: Curves.ease,\n                      );\n                    }\n                  : null,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass IntroPageWelcome extends StatelessWidget {\n  const IntroPageWelcome({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(32.0),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Text(\n            'Welcome',\n            style: Theme.of(context).textTheme.headlineLarge,\n          ),\n          const SizedBox(height: 24),\n          Text(\n            '''This app is based on the todo.txt format.\nIf you don't know what this is, you can find out more about the format below.''',\n            style: Theme.of(context).textTheme.bodyLarge,\n          ),\n          const SizedBox(height: 24),\n          FilledButton.tonal(\n            child: const Text('todo.txt format'),\n            onPressed: () async {\n              const String urlStr = 'https://github.com/todotxt/todo.txt';\n              final Uri url = Uri.parse(urlStr);\n              if (!await launchUrl(url)) {\n                throw Exception('Could not open $urlStr');\n              }\n            },\n          ),\n          const SizedBox(height: 24),\n          Text(\n            'You can use this app in different ways. Find out more on the following pages.',\n            style: Theme.of(context).textTheme.bodyLarge,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass IntroPageLocal extends StatelessWidget {\n  IntroPageLocal({super.key});\n\n  final List<(String, TextStyle?)> content = [\n    ('Use this app in ', null),\n    ('local', const TextStyle(fontWeight: FontWeight.bold)),\n    (' or ', null),\n    ('cloudless', const TextStyle(fontWeight: FontWeight.bold)),\n    (' mode.', null),\n    (\n      '\\nIn this mode, the app manages your todos locally only. This means that the app only reads and writes your todos from and to your todo file on your device.',\n      null\n    ),\n    (\n      '\\n\\nUse this option if you don\\'t need synchronisation across multiple devices, or if you have an external app that takes care of syncing the folder (e.g. Syncthing, Nextcloud, etc.).',\n      null\n    ),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(32.0),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Text(\n            'Local',\n            style: Theme.of(context).textTheme.headlineLarge,\n          ),\n          const SizedBox(height: 24),\n          RichText(\n            text: TextSpan(\n              style: Theme.of(context).textTheme.bodyLarge,\n              text: '',\n              children: <TextSpan>[\n                for ((String, TextStyle?) item in content)\n                  TextSpan(text: item.$1, style: item.$2),\n              ],\n            ),\n          ),\n          const SizedBox(height: 24),\n          FilledButton.tonal(\n            child: const Text('Use local mode'),\n            onPressed: () async {\n              Navigator.of(context).push(\n                MaterialPageRoute(\n                  builder: (BuildContext context) => const LocalLoginView(),\n                ),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass IntroPageWebDav extends StatelessWidget {\n  IntroPageWebDav({super.key});\n\n  final List<(String, TextStyle?)> content = [\n    ('Use this app in ', null),\n    ('webdav', const TextStyle(fontWeight: FontWeight.bold)),\n    (' or ', null),\n    ('cloud', const TextStyle(fontWeight: FontWeight.bold)),\n    (' mode.', null),\n    (\n      '\\nIn this mode, the app manages your todos with a webdav server of your choice. This means that the app reads and writes your todos from and to your todo file on your device and synchronizes this file with your backend server.',\n      null\n    )\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(32.0),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Text(\n            'WebDAV',\n            style: Theme.of(context).textTheme.headlineLarge,\n          ),\n          const SizedBox(height: 24),\n          RichText(\n            text: TextSpan(\n              style: Theme.of(context).textTheme.bodyLarge,\n              text: '',\n              children: <TextSpan>[\n                for ((String, TextStyle?) item in content)\n                  TextSpan(text: item.$1, style: item.$2),\n              ],\n            ),\n          ),\n          const SizedBox(height: 24),\n          FilledButton.tonal(\n            child: const Text('Use webdav mode'),\n            onPressed: () async {\n              Navigator.of(context).push(\n                MaterialPageRoute(\n                  builder: (BuildContext context) => const WebDAVLoginView(),\n                ),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/licenses/page/licenses_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/oss_licenses.dart';\n\nclass LicenceListPage extends StatelessWidget {\n  const LicenceListPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const Scaffold(\n      appBar: MainAppBar(title: 'Licenses'),\n      body: LicenseListView(),\n    );\n  }\n}\n\nclass LicenseListView extends StatelessWidget {\n  const LicenseListView({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      itemCount: allDependencies.length,\n      itemBuilder: (BuildContext context, int index) {\n        Package package = allDependencies[index];\n        return ListTile(\n          title: Text(package.name),\n          subtitle: Text(package.repository ?? (package.homepage ?? '')),\n          onTap: () {\n            Navigator.of(context).push(\n              MaterialPageRoute(\n                builder: (BuildContext context) {\n                  return LicenceDetailPage(\n                    title: package.name,\n                    licence: package.license ?? package.description,\n                  );\n                },\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass LicenceDetailPage extends StatelessWidget {\n  final String title;\n  final String licence;\n\n  const LicenceDetailPage({\n    super.key,\n    required this.title,\n    required this.licence,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: MainAppBar(title: title),\n      body: Center(\n        child: Container(\n          padding: const EdgeInsets.all(16.0),\n          child: SingleChildScrollView(\n            physics: const BouncingScrollPhysics(),\n            child: Column(\n              children: [\n                Text(licence),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/login/page/login_page.dart",
    "content": "import 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/constants/app.dart';\nimport 'package:ntodotxt/common/misc.dart';\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/info_dialog.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\nimport 'package:permission_handler/permission_handler.dart';\n\nclass LocalLoginView extends StatefulWidget {\n  const LocalLoginView({super.key});\n\n  @override\n  State<LocalLoginView> createState() => _LocalLoginViewState();\n}\n\nclass _LocalLoginViewState extends State<LocalLoginView> {\n  bool loading = false;\n\n  @override\n  Widget build(BuildContext context) {\n    bool keyboardIsOpen = MediaQuery.of(context).viewInsets.bottom != 0;\n\n    return Stack(\n      children: [\n        Scaffold(\n          appBar: const MainAppBar(title: 'Local'),\n          body: ListView(\n            padding: const EdgeInsets.symmetric(horizontal: 8.0),\n            children: [\n              ListTile(\n                title: Text(\n                  'Todo',\n                  style: Theme.of(context).textTheme.titleSmall,\n                ),\n              ),\n              const TodoFilenameInput(),\n              const LocalPathInput(),\n            ],\n          ),\n          floatingActionButton: keyboardIsOpen\n              ? null\n              : BlocBuilder<TodoFileCubit, TodoFileState>(\n                  builder: (BuildContext context, TodoFileState state) {\n                    return FloatingActionButton.extended(\n                      heroTag: 'localUsage',\n                      icon: const Icon(Icons.done),\n                      label: const Text('Apply'),\n                      tooltip: 'Apply',\n                      onPressed: () async {\n                        try {\n                          setState(() => loading = true);\n                          await context.read<LoginCubit>().loginLocal(\n                                localTodoFilePath: state.localTodoFilePath,\n                              );\n                        } finally {\n                          setState(() => loading = false);\n                        }\n                      },\n                    );\n                  },\n                ),\n        ),\n        if (loading)\n          const Opacity(\n            opacity: 0.8,\n            child: ModalBarrier(dismissible: false, color: Colors.black),\n          ),\n        if (loading)\n          const Center(\n            child: CircularProgressIndicator(),\n          ),\n      ],\n    );\n  }\n}\n\nclass WebDAVLoginView extends StatefulWidget {\n  const WebDAVLoginView({super.key});\n\n  @override\n  State<WebDAVLoginView> createState() => _WebDAVLoginViewState();\n}\n\nclass _WebDAVLoginViewState extends State<WebDAVLoginView> {\n  bool loading = false;\n  String serverAddr = '';\n  String path = '';\n  String username = '';\n  String password = '';\n  bool acceptUntrustedCert = false;\n\n  late GlobalKey<FormState> formKey;\n\n  @override\n  void initState() {\n    super.initState();\n    formKey = GlobalKey<FormState>();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  void setServerAddr(String value) => setState(() => serverAddr = value);\n\n  void setAcceptUntrustedCert(bool value) =>\n      setState(() => acceptUntrustedCert = value);\n\n  void setBaseUrl(String value) => setState(() => path = value);\n\n  void setUsername(String value) => setState(() => username = value);\n\n  void setPassword(String value) => setState(() => password = value);\n\n  @override\n  Widget build(BuildContext context) {\n    bool keyboardIsOpen = MediaQuery.of(context).viewInsets.bottom != 0;\n\n    return GestureDetector(\n      onTap: () {\n        FocusScopeNode currentFocus = FocusScope.of(context);\n        if (!currentFocus.hasPrimaryFocus) {\n          currentFocus.unfocus();\n        }\n      },\n      child: Stack(\n        children: [\n          Scaffold(\n            appBar: const MainAppBar(title: 'WebDAV'),\n            body: Form(\n              key: formKey,\n              child: ListView(\n                padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                children: [\n                  ListTile(\n                    title: Text(\n                      'Server connection',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                  ServerAddrField(\n                    callback: setServerAddr,\n                  ),\n                  AcceptUntrustedCertField(\n                    callback: setAcceptUntrustedCert,\n                  ),\n                  BaseUrlField(\n                    callback: setBaseUrl,\n                  ),\n                  UsernameField(\n                    callback: setUsername,\n                  ),\n                  PasswordField(\n                    callback: setPassword,\n                  ),\n                  const Divider(),\n                  ListTile(\n                    title: Text(\n                      'Todo',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                  const TodoFilenameInput(),\n                  const LocalPathInput(),\n                  const RemotePathInput(),\n                  const SizedBox(height: 72),\n                ],\n              ),\n            ),\n            floatingActionButton: keyboardIsOpen\n                ? null\n                : BlocBuilder<TodoFileCubit, TodoFileState>(\n                    builder: (BuildContext context, TodoFileState state) {\n                      return FloatingActionButton.extended(\n                        heroTag: 'webdavUsage',\n                        icon: const Icon(Icons.done),\n                        label: const Text('Apply'),\n                        tooltip: 'Apply',\n                        onPressed: () async {\n                          if (formKey.currentState!.validate()) {\n                            try {\n                              setState(() => loading = true);\n                              await context.read<LoginCubit>().loginWebDAV(\n                                    localTodoFilePath: state.localTodoFilePath,\n                                    remoteTodoFilePath:\n                                        state.remoteTodoFilePath,\n                                    server: serverAddr,\n                                    path: path,\n                                    username: username,\n                                    password: password,\n                                    acceptUntrustedCert: acceptUntrustedCert,\n                                  );\n                            } finally {\n                              setState(() => loading = false);\n                            }\n                          }\n                        },\n                      );\n                    },\n                  ),\n          ),\n          if (loading)\n            const Opacity(\n              opacity: 0.8,\n              child: ModalBarrier(dismissible: false, color: Colors.black),\n            ),\n          if (loading)\n            const Center(\n              child: CircularProgressIndicator(),\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass ServerAddrField extends StatefulWidget {\n  final Function(String serverAddr) callback;\n\n  const ServerAddrField({\n    required this.callback,\n    super.key,\n  });\n\n  @override\n  State<ServerAddrField> createState() => _ServerAddrFieldState();\n}\n\nclass _ServerAddrFieldState extends State<ServerAddrField> {\n  late TextEditingController textFieldController;\n\n  @override\n  void initState() {\n    super.initState();\n    textFieldController = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: const Icon(Icons.dns),\n      title: TextFormField(\n        controller: textFieldController,\n        style: Theme.of(context).textTheme.bodyMedium,\n        textCapitalization: TextCapitalization.none,\n        decoration: const InputDecoration(\n          labelText: 'Server',\n          hintText: 'http[s]://server[:port]',\n        ),\n        validator: (String? value) {\n          if (value == null || value.isEmpty) {\n            return 'Missing server address';\n          }\n          if (!value.startsWith('http://') && !value.startsWith('https://')) {\n            return 'Missing protocol';\n          }\n          if (!RegExp(\n                  r'(?<proto>^(http|https):\\/\\/)(?<host>[a-zA-Z0-9.-]+)(:(?<port>\\d+)){0,1}$')\n              .hasMatch(value)) {\n            return 'Invalid format';\n          }\n          return null;\n        },\n        onChanged: (String value) => widget.callback(textFieldController.text),\n      ),\n    );\n  }\n}\n\nclass AcceptUntrustedCertField extends StatefulWidget {\n  final Function(bool allowSelfSignedCert) callback;\n\n  const AcceptUntrustedCertField({\n    required this.callback,\n    super.key,\n  });\n\n  @override\n  State<AcceptUntrustedCertField> createState() =>\n      _AcceptUntrustedCertFieldState();\n}\n\nclass _AcceptUntrustedCertFieldState extends State<AcceptUntrustedCertField> {\n  bool checked = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: Icon(!checked ? Icons.lock_outline : Icons.lock_open),\n      title: const Text('Allow certificate'),\n      subtitle: const Text('Auto-accept certificate'),\n      trailing: Checkbox(\n        value: checked,\n        onChanged: (bool? value) {\n          setState(() => checked = value ?? true);\n          widget.callback(value ?? true);\n        },\n      ),\n    );\n  }\n}\n\nclass BaseUrlField extends StatefulWidget {\n  final Function(String path) callback;\n\n  const BaseUrlField({\n    required this.callback,\n    super.key,\n  });\n\n  @override\n  State<BaseUrlField> createState() => _BaseUrlFieldState();\n}\n\nclass _BaseUrlFieldState extends State<BaseUrlField> {\n  late TextEditingController textFieldController;\n\n  @override\n  void initState() {\n    super.initState();\n    textFieldController = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: const Icon(Icons.http),\n      title: TextFormField(\n        controller: textFieldController,\n        style: Theme.of(context).textTheme.bodyMedium,\n        textCapitalization: TextCapitalization.none,\n        decoration: const InputDecoration(\n          labelText: 'Path',\n          hintText: '/remote.php/dav/files/<username>',\n        ),\n        validator: (String? value) {\n          if (value == null || value.isEmpty) {\n            return 'Missing path';\n          }\n          return null;\n        },\n        onChanged: (String value) => widget.callback(textFieldController.text),\n      ),\n      trailing: IconButton(\n        icon: const Icon(Icons.help_outline),\n        onPressed: () => InfoDialog.dialog(\n          context: context,\n          title: 'Path',\n          message:\n              '''The username is not automatically appended to the path. In some cases path containing the username is expected, in others this causes an error.\n\nPlease check the requirements of your webdav server.''',\n        ),\n      ),\n    );\n  }\n}\n\nclass UsernameField extends StatefulWidget {\n  final Function(String username) callback;\n\n  const UsernameField({\n    required this.callback,\n    super.key,\n  });\n\n  @override\n  State<UsernameField> createState() => _UsernameFieldState();\n}\n\nclass _UsernameFieldState extends State<UsernameField> {\n  late TextEditingController textFieldController;\n\n  @override\n  void initState() {\n    super.initState();\n    textFieldController = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: const Icon(Icons.person),\n      title: TextFormField(\n        controller: textFieldController,\n        style: Theme.of(context).textTheme.bodyMedium,\n        textCapitalization: TextCapitalization.none,\n        decoration: const InputDecoration(\n          labelText: 'Username',\n          hintText: 'Username',\n        ),\n        validator: (String? value) {\n          if (value == null || value.isEmpty) {\n            return 'Missing username';\n          }\n          return null;\n        },\n        onChanged: (String value) => widget.callback(textFieldController.text),\n      ),\n    );\n  }\n}\n\nclass PasswordField extends StatefulWidget {\n  final Function(String password) callback;\n\n  const PasswordField({\n    required this.callback,\n    super.key,\n  });\n\n  @override\n  State<PasswordField> createState() => _PasswordFieldState();\n}\n\nclass _PasswordFieldState extends State<PasswordField> {\n  late TextEditingController textFieldController;\n\n  bool showPassword = false;\n\n  @override\n  void initState() {\n    super.initState();\n    textFieldController = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      leading: const Icon(Icons.password),\n      title: TextFormField(\n        controller: textFieldController,\n        style: Theme.of(context).textTheme.bodyMedium,\n        textCapitalization: TextCapitalization.none,\n        decoration: const InputDecoration(\n          labelText: 'Password',\n          hintText: 'Password',\n        ),\n        obscureText: !showPassword,\n        validator: (String? value) {\n          if (value == null || value.isEmpty) {\n            return 'Missing password';\n          }\n          return null;\n        },\n        onChanged: (String value) => widget.callback(textFieldController.text),\n      ),\n      trailing: IconButton(\n        icon: Icon(\n          showPassword ? Icons.visibility_off : Icons.visibility,\n        ),\n        onPressed: () => setState(() => showPassword = !showPassword),\n      ),\n    );\n  }\n}\n\nclass LocalPathInput extends StatelessWidget {\n  const LocalPathInput({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      buildWhen: (TodoFileState previousState, TodoFileState state) =>\n          previousState.localPath != state.localPath,\n      builder: (BuildContext context, TodoFileState state) {\n        return ListTile(\n          leading: const Icon(Icons.folder),\n          title: Text(\n            'Local path',\n            style: Theme.of(context).textTheme.bodySmall,\n          ),\n          subtitle: Text(state.localPath),\n          trailing: IconButton(\n            icon: const Icon(Icons.help_outline),\n            onPressed: () => InfoDialog.dialog(\n              context: context,\n              title: 'Local path',\n              message: '''Choose a directory by tapping the current local path.\n\nUse this option if it's important to you where your todos are stored on your device. Otherwise, the app's cache directory is used.''',\n            ),\n          ),\n          onTap: () async {\n            if (!PlatformInfo.isAppOS ||\n                await Permission.manageExternalStorage.request().isGranted ||\n                await Permission.storage.request().isGranted) {\n              String? selectedDirectory =\n                  await FilePicker.platform.getDirectoryPath();\n              if (context.mounted) {\n                // If user canceled the directory picker use app cache directory as fallback.\n                await context\n                    .read<TodoFileCubit>()\n                    .saveLocalPath(selectedDirectory ?? state.localPath);\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n}\n\nclass RemotePathInput extends StatefulWidget {\n  const RemotePathInput({super.key});\n\n  @override\n  State<RemotePathInput> createState() => _RemotePathInputState();\n}\n\nclass _RemotePathInputState extends State<RemotePathInput> {\n  final TextEditingController controller = TextEditingController();\n  final Debouncer debounce = Debouncer(milliseconds: 1000);\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    debounce.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // Initial value\n    controller.text = context.read<TodoFileCubit>().state.remotePath;\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      builder: (BuildContext context, TodoFileState state) {\n        return ListTile(\n            leading: const Icon(Icons.folder),\n            title: TextFormField(\n              controller: controller,\n              style: Theme.of(context).textTheme.bodyMedium,\n              textCapitalization: TextCapitalization.none,\n              decoration: const InputDecoration(\n                labelText: 'Remote path',\n                hintText: defaultRemoteTodoPath,\n              ),\n              onChanged: (String value) async {\n                debounce.run(() async => await _save(context, value));\n              },\n            ),\n            trailing: IconButton(\n              icon: const Icon(Icons.help_outline),\n              onPressed: () => InfoDialog.dialog(\n                context: context,\n                title: 'Remote path',\n                message:\n                    'This remote path is appended to the base path of the server connection. This makes it possible to define a user-defined path for the todo files.',\n              ),\n            ));\n      },\n    );\n  }\n\n  Future<void> _save(BuildContext context, String value) async {\n    if (value.isEmpty) {\n      SnackBarHandler.info(\n        context,\n        'Empty remote path is not allowed. Using default one.',\n      );\n      await context.read<TodoFileCubit>().saveRemotePath(defaultRemoteTodoPath);\n      controller.value = controller.value.copyWith(\n        text: defaultRemoteTodoPath,\n        selection:\n            const TextSelection.collapsed(offset: defaultRemoteTodoPath.length),\n      );\n    } else {\n      await context.read<TodoFileCubit>().saveRemotePath(value);\n      controller.value = controller.value.copyWith(\n        text: value,\n        selection: TextSelection.collapsed(offset: value.length),\n      );\n    }\n  }\n}\n\nclass TodoFilenameInput extends StatefulWidget {\n  const TodoFilenameInput({super.key});\n\n  @override\n  State<TodoFilenameInput> createState() => _TodoFilenameInputState();\n}\n\nclass _TodoFilenameInputState extends State<TodoFilenameInput> {\n  final TextEditingController controller = TextEditingController();\n  final Debouncer debounce = Debouncer(milliseconds: 1000);\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    debounce.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // Initial value\n    controller.text = context.read<TodoFileCubit>().state.todoFilename;\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      builder: (BuildContext context, TodoFileState state) {\n        return ListTile(\n          leading: const Icon(Icons.description),\n          title: TextFormField(\n            controller: controller,\n            style: Theme.of(context).textTheme.bodyMedium,\n            textCapitalization: TextCapitalization.none,\n            decoration: const InputDecoration(\n              labelText: 'Todo filename',\n              hintText: defaultTodoFilename,\n            ),\n            onChanged: (String value) async {\n              debounce.run(() async => await _save(context, value));\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _save(BuildContext context, String value) async {\n    if (value.isEmpty) {\n      SnackBarHandler.info(\n        context,\n        'Empty todo filename is not allowed. Using default one.',\n      );\n      await context\n          .read<TodoFileCubit>()\n          .saveLocalFilename(defaultTodoFilename);\n      controller.value = controller.value.copyWith(\n        text: defaultTodoFilename,\n        selection:\n            const TextSelection.collapsed(offset: defaultTodoFilename.length),\n      );\n    } else {\n      await context.read<TodoFileCubit>().saveLocalFilename(value);\n      controller.value = controller.value.copyWith(\n        text: value,\n        selection: TextSelection.collapsed(offset: value.length),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "lib/login/state/login_cubit.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/login/state/login_state.dart';\nimport 'package:ntodotxt/main.dart' show secureStorage;\nimport 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\n\nclass LoginCubit extends Cubit<LoginState> {\n  LoginCubit({\n    LoginState? state,\n  }) : super(state ?? const LoginLoading());\n\n  Future<void> login() async {\n    try {\n      emit(state.loading());\n      String? backendFromsecureStorage =\n          await secureStorage.read(key: 'backend');\n      Backend backend;\n\n      if (backendFromsecureStorage == null) {\n        return emit(state.logout());\n      }\n\n      try {\n        backend = Backend.values.byName(backendFromsecureStorage);\n      } on Exception {\n        return emit(state.logout());\n      }\n\n      if (backend == Backend.none) {\n        return emit(state.logout());\n      }\n\n      if (backend == Backend.local) {\n        return emit(state.loginLocal());\n      }\n      if (backend == Backend.webdav) {\n        String? server = await secureStorage.read(key: 'server');\n        String? path = await secureStorage.read(key: 'path');\n        String? username = await secureStorage.read(key: 'username');\n        String? password = await secureStorage.read(key: 'password');\n        bool acceptUntrustedCert =\n            (await secureStorage.read(key: 'acceptUntrustedCert')) == '1'\n                ? true\n                : false;\n        if (server != null &&\n            path != null &&\n            username != null &&\n            password != null) {\n          emit(\n            state.loginWebDAV(\n              server: server,\n              path: path,\n              username: username,\n              password: password,\n              acceptUntrustedCert: acceptUntrustedCert,\n            ),\n          );\n        }\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> logout() async {\n    try {\n      await resetSecureStorage();\n      emit(state.logout());\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> loginLocal({\n    required String localTodoFilePath,\n  }) async {\n    try {\n      LocalTodoListApi.fromString(\n          localFilePath: localTodoFilePath); // Check before login.\n      await resetSecureStorage();\n      await secureStorage.write(key: 'backend', value: Backend.local.name);\n      emit(state.loginLocal());\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> loginWebDAV({\n    required String localTodoFilePath,\n    required String remoteTodoFilePath,\n    required String server,\n    required String path,\n    required String username,\n    required String password,\n    required bool acceptUntrustedCert,\n  }) async {\n    try {\n      WebDAVClient client = WebDAVClient(\n        server: server,\n        path: path,\n        username: username,\n        password: password,\n        acceptUntrustedCert: acceptUntrustedCert,\n      );\n      WebDAVTodoListApi api = WebDAVTodoListApi.fromString(\n        localFilePath: localTodoFilePath,\n        remoteFilePath: remoteTodoFilePath,\n        client: client,\n      );\n      await api.client.ping();\n      await api.client.listFiles();\n      await resetSecureStorage();\n      await secureStorage.write(key: 'backend', value: Backend.webdav.name);\n      await secureStorage.write(key: 'server', value: server);\n      await secureStorage.write(key: 'path', value: path);\n      await secureStorage.write(key: 'username', value: username);\n      await secureStorage.write(key: 'password', value: password);\n      await secureStorage.write(\n          key: 'acceptUntrustedCert', value: acceptUntrustedCert ? '1' : '0');\n      emit(\n        state.loginWebDAV(\n          server: server,\n          path: path,\n          username: username,\n          password: password,\n          acceptUntrustedCert: acceptUntrustedCert,\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> resetSecureStorage() async {\n    try {\n      final List<String> attrs = [\n        'backend',\n        'server',\n        'path',\n        'username',\n        'password',\n        'acceptUntrustedCert',\n      ];\n      for (var attr in attrs) {\n        final String? value = await secureStorage.read(key: attr);\n        if (value != null) {\n          await secureStorage.delete(key: attr);\n        }\n      }\n      emit(state.logout());\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/login/state/login_state.dart",
    "content": "import 'dart:math';\n\nimport 'package:equatable/equatable.dart';\n\nenum Backend { none, local, webdav }\n\nsealed class LoginState extends Equatable {\n  final Backend backend; // Backend to use to store todos.\n\n  const LoginState({\n    this.backend = Backend.none,\n  });\n\n  LoginState copyWith();\n\n  LoginLoading loading() => const LoginLoading();\n\n  Logout logout() => const Logout();\n\n  LoginLocal loginLocal() => const LoginLocal();\n\n  LoginWebDAV loginWebDAV({\n    required String server,\n    required String path,\n    required String username,\n    required String password,\n    required bool acceptUntrustedCert,\n  }) =>\n      LoginWebDAV(\n        server: server,\n        path: path,\n        username: username,\n        password: password,\n        acceptUntrustedCert: acceptUntrustedCert,\n      );\n\n  LoginError error({\n    required String message,\n  }) =>\n      LoginError(message: message);\n\n  @override\n  List<Object> get props => [\n        backend,\n      ];\n\n  @override\n  String toString() => 'LoginState { }';\n}\n\nfinal class LoginLoading extends LoginState {\n  const LoginLoading({\n    super.backend,\n  });\n\n  @override\n  LoginLoading copyWith() => super.loading();\n\n  @override\n  List<Object> get props => [\n        backend,\n      ];\n\n  @override\n  String toString() => 'LoginLoading { }';\n}\n\nfinal class Logout extends LoginState {\n  const Logout({\n    super.backend = Backend.none,\n  });\n\n  @override\n  Logout copyWith() => super.logout();\n\n  @override\n  List<Object> get props => [\n        backend,\n      ];\n\n  @override\n  String toString() => 'Logout { }';\n}\n\nfinal class LoginLocal extends LoginState {\n  const LoginLocal({\n    super.backend = Backend.local,\n  });\n\n  @override\n  LoginLocal copyWith() => super.loginLocal();\n\n  @override\n  List<Object> get props => [\n        backend,\n      ];\n\n  @override\n  String toString() => 'LoginLocal { }';\n}\n\nfinal class LoginWebDAV extends LoginState {\n  /// Backend server.\n  final String server;\n\n  /// Path.\n  final String path;\n\n  /// Backend username.\n  final String username;\n\n  /// Backend password.\n  final String password;\n\n  /// Accept untrusted certificate.\n  final bool acceptUntrustedCert;\n\n  const LoginWebDAV({\n    super.backend = Backend.webdav,\n    required this.server,\n    required this.path,\n    required this.username,\n    required this.password,\n    required this.acceptUntrustedCert,\n  });\n\n  @override\n  LoginWebDAV copyWith({\n    String? server,\n    String? path,\n    String? username,\n    String? password,\n    bool? acceptUntrustedCert,\n  }) =>\n      super.loginWebDAV(\n        server: server ?? this.server,\n        path: path ?? this.path,\n        username: username ?? this.username,\n        password: password ?? this.password,\n        acceptUntrustedCert: acceptUntrustedCert ?? this.acceptUntrustedCert,\n      );\n\n  @override\n  List<Object> get props => [\n        backend,\n        server,\n        path,\n        username,\n        password,\n        acceptUntrustedCert,\n      ];\n\n  @override\n  String toString() => 'LoginWebDAV { }';\n}\n\nfinal class LoginError extends LoginState {\n  /// Random id to force updates/rebuilds\n  /// if there occure the same errors in row.\n  final int id;\n\n  /// Error message.\n  final String message;\n\n  LoginError({\n    required this.message,\n    super.backend,\n  }) : id = Random().nextInt(999);\n\n  @override\n  LoginError copyWith({\n    String? message,\n  }) =>\n      super.error(message: message ?? this.message);\n\n  @override\n  List<Object> get props => [\n        id,\n        backend,\n        message,\n      ];\n\n  @override\n  String toString() => 'LoginError { message: $message }';\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:flutter_secure_storage/flutter_secure_storage.dart';\nimport 'package:logging/logging.dart';\nimport 'package:ntodotxt/common/bloc_observer.dart' show GenericBlocObserver;\nimport 'package:ntodotxt/common/misc.dart' show SnackBarHandler;\nimport 'package:ntodotxt/common/router/router.dart';\nimport 'package:ntodotxt/common/theme/theme.dart' show lightTheme, darkTheme;\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart'\n    show FilterController;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart'\n    show FilterRepository;\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/filter/state/filter_list_state.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/intro/page/intro_page.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/login/state/login_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/todo/repository/todo_list_repository.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\nimport 'package:path/path.dart' as path;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nfinal Logger log = Logger('ntodotxt');\nconst FlutterSecureStorage secureStorage = FlutterSecureStorage(\n  // Pass the option to the constructor\n  iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),\n  aOptions: AndroidOptions(encryptedSharedPreferences: true),\n);\n\nvoid main() async {\n  // Avoid errors caused by flutter upgrade.\n  // Importing 'package:flutter/widgets.dart' is required.\n  WidgetsFlutterBinding.ensureInitialized();\n  await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n\n  if (Platform.isWindows || Platform.isLinux) {\n    // Initialize FFI\n    sqfliteFfiInit();\n  }\n\n  Logger.root.level = Level.FINER; // defaults to Level.INFO\n  Logger.root.onRecord.listen((record) {\n    // ignore: avoid_print\n    print('${record.level.name}: ${record.time}: ${record.message}');\n  });\n\n  log.info('Initialize main');\n\n  log.info('Setup bloc oberserver');\n  Bloc.observer = GenericBlocObserver();\n  Directory appDataDir = await getApplicationDocumentsDirectory();\n\n  log.info('Run app');\n  runApp(\n    App(appDataDir: appDataDir.path),\n  );\n}\n\nclass App extends StatefulWidget {\n  final String appDataDir;\n\n  const App({\n    required this.appDataDir,\n    super.key,\n  });\n\n  @override\n  State<App> createState() => _AppState();\n}\n\nclass _AppState extends State<App> {\n  late final DatabaseController dbController;\n\n  @override\n  void initState() {\n    super.initState();\n    dbController = DatabaseController(path.join(widget.appDataDir, 'data.db'));\n  }\n\n  @override\n  void dispose() {\n    dbController.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final DatabaseController dbController = DatabaseController(\n      path.join(widget.appDataDir, 'data.db'),\n    );\n\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: MultiBlocProvider(\n        providers: [\n          BlocProvider<LoginCubit>(\n            create: (BuildContext context) => LoginCubit(),\n          ),\n          BlocProvider<TodoFileCubit>(\n            create: (BuildContext context) => TodoFileCubit(\n              repository: context.read<SettingRepository>(),\n              localPath: widget.appDataDir,\n            ),\n          ),\n          BlocProvider<DrawerCubit>(\n            create: (BuildContext context) => DrawerCubit(),\n          ),\n          // Default filter\n          BlocProvider<FilterCubit>(\n            create: (BuildContext context) => FilterCubit(\n              settingRepository: context.read<SettingRepository>(),\n              filterRepository: context.read<FilterRepository>(),\n            ),\n          ),\n          BlocProvider<FilterListBloc>(\n            create: (BuildContext context) => FilterListBloc(\n              repository: context.read<FilterRepository>(),\n            ),\n          ),\n          BlocProvider<InteractionSettingsCubit>(\n            create: (BuildContext context) => InteractionSettingsCubit(\n              repository: context.read<SettingRepository>(),\n            ),\n          ),\n        ],\n        child: Builder(\n          builder: (BuildContext context) {\n            return BlocBuilder<LoginCubit, LoginState>(\n              builder: (BuildContext context, LoginState state) {\n                if (state is LoginLocal || state is LoginWebDAV) {\n                  return CoreApp(loginState: state);\n                } else {\n                  return const InitialApp();\n                }\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass InitialApp extends StatelessWidget {\n  final ThemeMode? themeMode;\n\n  const InitialApp({\n    this.themeMode,\n    super.key,\n  });\n\n  Future<bool> _initialize(BuildContext context) async {\n    if (context.mounted) {\n      await context.read<InteractionSettingsCubit>().load();\n    }\n    if (context.mounted) {\n      await context.read<FilterCubit>().load();\n    }\n    if (context.mounted) {\n      await context.read<TodoFileCubit>().load();\n    }\n    if (context.mounted) {\n      context.read<FilterListBloc>()\n        ..add(const FilterListSubscriped())\n        ..add(const FilterListSynchronizationRequested());\n    }\n    if (context.mounted) {\n      await context.read<LoginCubit>().login();\n    }\n\n    return true;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      theme: lightTheme,\n      darkTheme: darkTheme,\n      themeMode: themeMode,\n      localizationsDelegates: [\n        GlobalMaterialLocalizations.delegate,\n        GlobalWidgetsLocalizations.delegate,\n        GlobalCupertinoLocalizations.delegate,\n      ],\n      supportedLocales: [\n        Locale('en'),\n        Locale('de'),\n      ],\n      home: MultiBlocListener(\n        listeners: [\n          BlocListener<LoginCubit, LoginState>(\n            listener: (BuildContext context, LoginState state) {\n              if (state is LoginError) {\n                SnackBarHandler.error(context, state.message);\n              }\n            },\n          ),\n          BlocListener<TodoFileCubit, TodoFileState>(\n            listener: (BuildContext context, TodoFileState state) {\n              if (state is TodoFileError) {\n                SnackBarHandler.error(context, state.message);\n              }\n            },\n          ),\n          BlocListener<FilterListBloc, FilterListState>(\n            listener: (BuildContext context, FilterListState state) {\n              if (state is FilterListError) {\n                SnackBarHandler.error(context, state.message);\n              }\n            },\n          ),\n          BlocListener<FilterCubit, FilterState>(\n            listener: (BuildContext context, FilterState state) {\n              if (state is FilterError) {\n                SnackBarHandler.error(context, state.message);\n              }\n            },\n          ),\n        ],\n        child: FutureBuilder<bool>(\n          future: _initialize(context),\n          builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {\n            if (snapshot.hasData) {\n              return BlocBuilder<LoginCubit, LoginState>(\n                builder: (BuildContext context, LoginState state) {\n                  if (state is LoginLoading ||\n                      state is LoginLocal ||\n                      state is LoginWebDAV) {\n                    // Keep loading screen to prevent screen flickering.\n                    return _loadingScreen();\n                  } else {\n                    return const IntroPage();\n                  }\n                },\n              );\n            } else if (snapshot.hasError) {\n              return _errorScreen();\n            } else {\n              return _loadingScreen();\n            }\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _loadingScreen() {\n    return Scaffold(\n      body: Center(\n        child: FractionallySizedBox(\n          widthFactor: 0.5,\n          heightFactor: 0.5,\n          child: Image.asset('assets/icon/icon.png'),\n        ),\n      ),\n    );\n  }\n\n  Widget _errorScreen() {\n    return const Scaffold(\n      body: Center(\n        child: Text('Something went wrong.'),\n      ),\n    );\n  }\n}\n\nclass CoreApp extends StatelessWidget {\n  final LoginState loginState;\n  final ThemeMode? themeMode;\n\n  const CoreApp({\n    required this.loginState,\n    this.themeMode,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      builder: (BuildContext context, TodoFileState todoFileState) {\n        final TodoListRepository todoListRepository =\n            _createTodoListRepository(loginState, todoFileState);\n        final TodoListBloc todoListBloc = TodoListBloc(\n          repository: todoListRepository,\n        )\n          ..add(const TodoListSubscriptionRequested())\n          ..add(const TodoListSynchronizationRequested());\n        return RepositoryProvider(\n          create: (BuildContext context) => todoListRepository,\n          child: BlocProvider.value(\n            value: todoListBloc,\n            child: Builder(\n              builder: (BuildContext context) {\n                return MaterialApp.router(\n                  title: 'ntodotxt',\n                  debugShowCheckedModeBanner: false,\n                  theme: lightTheme,\n                  darkTheme: darkTheme,\n                  themeMode: themeMode,\n                  localizationsDelegates: [\n                    GlobalMaterialLocalizations.delegate,\n                    GlobalWidgetsLocalizations.delegate,\n                    GlobalCupertinoLocalizations.delegate,\n                  ],\n                  supportedLocales: [\n                    Locale('en'),\n                    Locale('de'),\n                  ],\n                  routerConfig: AppRouter().config,\n                );\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  TodoListRepository _createTodoListRepository(\n      LoginState loginState, TodoFileState todoFileState) {\n    late TodoListApi api;\n    switch (loginState) {\n      case LoginLocal():\n        log.info('Use local backend');\n        log.info('Using local todo file ${todoFileState.localTodoFilePath}');\n        api = LocalTodoListApi.fromString(\n          localFilePath: todoFileState.localTodoFilePath,\n        );\n      case LoginWebDAV():\n        log.info('Use local+webdav backend');\n        log.info('Using remote todo file ${todoFileState.localTodoFilePath}');\n        api = WebDAVTodoListApi.fromString(\n          localFilePath: todoFileState.localTodoFilePath,\n          remoteFilePath: todoFileState.remoteTodoFilePath,\n          client: WebDAVClient(\n            server: loginState.server,\n            path: loginState.path,\n            username: loginState.username,\n            password: loginState.password,\n            acceptUntrustedCert: loginState.acceptUntrustedCert,\n          ),\n        );\n      default:\n        log.info('Fallback to local backend');\n        log.info('Using local todo file ${todoFileState.localTodoFilePath}');\n        api = LocalTodoListApi.fromString(\n          localFilePath: todoFileState.localTodoFilePath,\n        );\n    }\n\n    return TodoListRepository(api);\n  }\n}\n"
  },
  {
    "path": "lib/oss_licenses.dart",
    "content": "// coverage:ignore-file\n\n// cSpell:disable\n// ignore_for_file: always_put_required_named_parameters_first\n// ignore_for_file: constant_identifier_names\n// ignore_for_file: sort_constructors_first\n\n// This code was generated by flutter_oss_licenses\n// https://pub.dev/packages/flutter_oss_licenses\n\n/// All dependencies including transitive dependencies.\nconst allDependencies = <Package>[\n  _archive,\n  _args,\n  _async,\n  _bloc,\n  _boolean_selector,\n  _characters,\n  _checked_yaml,\n  _cli_util,\n  _clock,\n  _collection,\n  _convert,\n  _cross_file,\n  _crypto,\n  _cupertino_icons,\n  _dart_pubspec_licenses,\n  _dio,\n  _dio_web_adapter,\n  _equatable,\n  _fake_async,\n  _ffi,\n  _file,\n  _file_picker,\n  _flex_color_scheme,\n  _flex_seed_scheme,\n  _flutter,\n  _flutter_bloc,\n  _flutter_launcher_icons,\n  _flutter_lints,\n  _flutter_oss_licenses,\n  _flutter_plugin_android_lifecycle,\n  _flutter_secure_storage,\n  _flutter_secure_storage_linux,\n  _flutter_secure_storage_macos,\n  _flutter_secure_storage_platform_interface,\n  _flutter_secure_storage_web,\n  _flutter_secure_storage_windows,\n  _go_router,\n  _http_parser,\n  _image,\n  _intl,\n  _js,\n  _json_annotation,\n  _leak_tracker,\n  _leak_tracker_flutter_testing,\n  _leak_tracker_testing,\n  _lints,\n  _logging,\n  _matcher,\n  _material_color_utilities,\n  _meta,\n  _mime,\n  _nested,\n  _path,\n  _path_provider,\n  _path_provider_android,\n  _path_provider_foundation,\n  _path_provider_linux,\n  _path_provider_platform_interface,\n  _path_provider_windows,\n  _permission_handler,\n  _permission_handler_android,\n  _permission_handler_apple,\n  _permission_handler_html,\n  _permission_handler_platform_interface,\n  _permission_handler_windows,\n  _petitparser,\n  _platform,\n  _plugin_platform_interface,\n  _posix,\n  _process,\n  _provider,\n  _rxdart,\n  _source_span,\n  _sqflite_common,\n  _sqflite_common_ffi,\n  _sqlite3,\n  _sqlite3_flutter_libs,\n  _stack_trace,\n  _stream_channel,\n  _string_scanner,\n  _sync_http,\n  _synchronized,\n  _term_glyph,\n  _test_api,\n  _test_cov_console,\n  _typed_data,\n  _url_launcher,\n  _url_launcher_android,\n  _url_launcher_ios,\n  _url_launcher_linux,\n  _url_launcher_macos,\n  _url_launcher_platform_interface,\n  _url_launcher_web,\n  _url_launcher_windows,\n  _vector_math,\n  _vm_service,\n  _web,\n  _webdav_client,\n  _webdriver,\n  _win32,\n  _xdg_directories,\n  _xml,\n  _yaml\n];\n\n/// Direct `dependencies`.\nconst dependencies = <Package>[\n  _collection,\n  _crypto,\n  _cupertino_icons,\n  _dio,\n  _equatable,\n  _file_picker,\n  _flex_color_scheme,\n  _flutter,\n  _flutter_bloc,\n  _flutter_secure_storage,\n  _go_router,\n  _intl,\n  _logging,\n  _path,\n  _path_provider,\n  _permission_handler,\n  _rxdart,\n  _sqflite_common_ffi,\n  _sqlite3_flutter_libs,\n  _url_launcher,\n  _webdav_client\n];\n\n/// Direct `dev_dependencies`.\nconst devDependencies = <Package>[\n  _file,\n  _flutter_launcher_icons,\n  _flutter_lints,\n  _flutter_oss_licenses,\n  _test_cov_console\n];\n\n/// Package license definition.\nclass Package {\n  /// Package name\n  final String name;\n\n  /// Description\n  final String description;\n\n  /// Website URL\n  final String? homepage;\n\n  /// Repository URL\n  final String? repository;\n\n  /// Authors\n  final List<String> authors;\n\n  /// Version\n  final String version;\n\n  /// License\n  final String? license;\n\n  /// Whether the license is in markdown format or not (plain text).\n  final bool isMarkdown;\n\n  /// Whether the package is included in the SDK or not.\n  final bool isSdk;\n\n  /// Direct dependencies\n  final List<PackageRef> dependencies;\n\n  const Package({\n    required this.name,\n    required this.description,\n    this.homepage,\n    this.repository,\n    required this.authors,\n    required this.version,\n    this.license,\n    required this.isMarkdown,\n    required this.isSdk,\n    required this.dependencies,\n  });\n}\n\nclass PackageRef {\n  final String name;\n\n  const PackageRef(this.name);\n\n  Package resolve() => allDependencies.firstWhere((d) => d.name == name);\n}\n\n/// archive 4.0.7\nconst _archive = Package(\n    name: 'archive',\n    description:\n        'Provides encoders and decoders for various archive and compression formats such as zip, tar, bzip2, gzip, and zlib.',\n    repository: 'https://github.com/brendan-duncan/archive',\n    authors: [],\n    version: '4.0.7',\n    license: '''The MIT License\n\nCopyright (c) 2013-2021 Brendan Duncan.\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('crypto'),\n      PackageRef('path'),\n      PackageRef('posix')\n    ]);\n\n/// args 2.7.0\nconst _args = Package(\n    name: 'args',\n    description:\n        'Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.',\n    repository: 'https://github.com/dart-lang/core/tree/main/pkgs/args',\n    authors: [],\n    version: '2.7.0',\n    license: '''Copyright 2013, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// async 2.11.0\nconst _async = Package(\n    name: 'async',\n    description:\n        \"Utility functions and classes related to the 'dart:async' library.\",\n    repository: 'https://github.com/dart-lang/async',\n    authors: [],\n    version: '2.11.0',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('collection'), PackageRef('meta')]);\n\n/// bloc 9.0.0\nconst _bloc = Package(\n    name: 'bloc',\n    description:\n        'A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern.',\n    homepage: 'https://github.com/felangel/bloc',\n    repository: 'https://github.com/felangel/bloc/tree/master/packages/bloc',\n    authors: [],\n    version: '9.0.0',\n    license: '''The MIT License (MIT)\nCopyright (c) 2024 Felix Angelov\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of the Software,\nand to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\nUSE OR OTHER DEALINGS IN THE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta')]);\n\n/// boolean_selector 2.1.1\nconst _boolean_selector = Package(\n    name: 'boolean_selector',\n    description:\n        \"A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax.\",\n    repository: 'https://github.com/dart-lang/boolean_selector',\n    authors: [],\n    version: '2.1.1',\n    license: '''Copyright 2016, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// characters 1.3.0\nconst _characters = Package(\n    name: 'characters',\n    description:\n        'String replacement with operations that are Unicode/grapheme cluster aware.',\n    repository: 'https://github.com/dart-lang/characters',\n    authors: [],\n    version: '1.3.0',\n    license: '''Copyright 2019, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// checked_yaml 2.0.3\nconst _checked_yaml = Package(\n    name: 'checked_yaml',\n    description:\n        'Generate more helpful exceptions when decoding YAML documents using package:json_serializable and package:yaml.',\n    repository:\n        'https://github.com/google/json_serializable.dart/tree/master/checked_yaml',\n    authors: [],\n    version: '2.0.3',\n    license: '''Copyright 2019, the Dart project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('json_annotation'),\n      PackageRef('source_span'),\n      PackageRef('yaml')\n    ]);\n\n/// cli_util 0.4.2\nconst _cli_util = Package(\n    name: 'cli_util',\n    description: 'A library to help in building Dart command-line apps.',\n    repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/cli_util',\n    authors: [],\n    version: '0.4.2',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta'), PackageRef('path')]);\n\n/// clock 1.1.1\nconst _clock = Package(\n    name: 'clock',\n    description: 'A fakeable wrapper for dart:core clock APIs.',\n    repository: 'https://github.com/dart-lang/clock',\n    authors: [],\n    version: '1.1.1',\n    license: '''Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// collection 1.18.0\nconst _collection = Package(\n    name: 'collection',\n    description:\n        'Collections and utilities functions and classes related to collections.',\n    repository: 'https://github.com/dart-lang/collection',\n    authors: [],\n    version: '1.18.0',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// convert 3.1.2\nconst _convert = Package(\n    name: 'convert',\n    description:\n        'Utilities for converting between data representations. Provides a number of Sink, Codec, Decoder, and Encoder types.',\n    repository: 'https://github.com/dart-lang/core/tree/main/pkgs/convert',\n    authors: [],\n    version: '3.1.2',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('typed_data')]);\n\n/// cross_file 0.3.4+2\nconst _cross_file = Package(\n    name: 'cross_file',\n    description:\n        'An abstraction to allow working with files across multiple platforms.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/cross_file',\n    authors: [],\n    version: '0.3.4+2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta'), PackageRef('web')]);\n\n/// crypto 3.0.6\nconst _crypto = Package(\n    name: 'crypto',\n    description:\n        'Implementations of SHA, MD5, and HMAC cryptographic functions.',\n    repository: 'https://github.com/dart-lang/core/tree/main/pkgs/crypto',\n    authors: [],\n    version: '3.0.6',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('typed_data')]);\n\n/// cupertino_icons 1.0.8\nconst _cupertino_icons = Package(\n    name: 'cupertino_icons',\n    description:\n        'Default icons asset for Cupertino widgets based on Apple styled icons',\n    repository:\n        'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',\n    authors: [],\n    version: '1.0.8',\n    license: '''The MIT License (MIT)\n\nCopyright (c) 2016 Vladimir Kharlampidi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// dart_pubspec_licenses 3.0.4\nconst _dart_pubspec_licenses = Package(\n    name: 'dart_pubspec_licenses',\n    description:\n        'A library to make it easy to extract OSS license information from Dart packages using pubspec.yaml',\n    homepage:\n        'https://github.com/espresso3389/flutter_oss_licenses/tree/master/packages/dart_pubspec_licenses',\n    repository: 'https://github.com/espresso3389/flutter_oss_licenses',\n    authors: [],\n    version: '3.0.4',\n    license: '''MIT License\n\nCopyright (c) 2019 Takashi Kawasaki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('yaml'),\n      PackageRef('path'),\n      PackageRef('json_annotation')\n    ]);\n\n/// dio 5.9.0\nconst _dio = Package(\n    name: 'dio',\n    description: '''A powerful HTTP networking package,\nsupports Interceptors,\nAborting and canceling a request,\nCustom adapters, Transformers, etc.\n''',\n    homepage: 'https://github.com/cfug/dio',\n    repository: 'https://github.com/cfug/dio/blob/main/dio',\n    authors: [],\n    version: '5.9.0',\n    license: '''MIT License\n\nCopyright (c) 2018 Wen Du (wendux)\nCopyright (c) 2022 The CFUG Team\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('async'),\n      PackageRef('collection'),\n      PackageRef('http_parser'),\n      PackageRef('meta'),\n      PackageRef('mime'),\n      PackageRef('path'),\n      PackageRef('dio_web_adapter')\n    ]);\n\n/// dio_web_adapter 2.1.1\nconst _dio_web_adapter = Package(\n    name: 'dio_web_adapter',\n    description: 'An adapter that supports Dio on Web.',\n    homepage: 'https://github.com/cfug/dio',\n    repository: 'https://github.com/cfug/dio/blob/main/plugins/web_adapter',\n    authors: [],\n    version: '2.1.1',\n    license: '''MIT License\n\nCopyright (c) 2018 Wen Du (wendux)\nCopyright (c) 2022 The CFUG Team\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('dio'),\n      PackageRef('http_parser'),\n      PackageRef('meta'),\n      PackageRef('web')\n    ]);\n\n/// equatable 2.0.7\nconst _equatable = Package(\n    name: 'equatable',\n    description:\n        'A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.',\n    homepage: 'https://github.com/felangel/equatable',\n    repository: 'https://github.com/felangel/equatable',\n    authors: [],\n    version: '2.0.7',\n    license: '''MIT License\n\nCopyright (c) 2024 Felix Angelov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('collection'), PackageRef('meta')]);\n\n/// fake_async 1.3.1\nconst _fake_async = Package(\n    name: 'fake_async',\n    description:\n        'Fake asynchronous events such as timers and microtasks for deterministic testing.',\n    repository: 'https://github.com/dart-lang/fake_async',\n    authors: [],\n    version: '1.3.1',\n    license: '''Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// ffi 2.1.3\nconst _ffi = Package(\n    name: 'ffi',\n    description:\n        'Utilities for working with Foreign Function Interface (FFI) code.',\n    repository: 'https://github.com/dart-lang/native/tree/main/pkgs/ffi',\n    authors: [],\n    version: '2.1.3',\n    license: '''Copyright 2019, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// file 7.0.0\nconst _file = Package(\n    name: 'file',\n    description:\n        'A pluggable, mockable file system abstraction for Dart. Supports local file system access, as well as in-memory file systems, record-replay file systems, and chroot file systems.',\n    repository: 'https://github.com/google/file.dart/tree/master/packages/file',\n    authors: [],\n    version: '7.0.0',\n    license: '''Copyright 2017, the Dart project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta'), PackageRef('path')]);\n\n/// file_picker 10.1.9\nconst _file_picker = Package(\n    name: 'file_picker',\n    description:\n        'A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.',\n    homepage: 'https://github.com/miguelpruivo/plugins_flutter_file_picker',\n    repository: 'https://github.com/miguelpruivo/flutter_file_picker',\n    authors: [],\n    version: '10.1.9',\n    license: '''MIT License\n\nCopyright (c) 2018 Miguel Ruivo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('flutter_plugin_android_lifecycle'),\n      PackageRef('plugin_platform_interface'),\n      PackageRef('ffi'),\n      PackageRef('path'),\n      PackageRef('win32'),\n      PackageRef('cross_file'),\n      PackageRef('web')\n    ]);\n\n/// flex_color_scheme 8.0.2\nconst _flex_color_scheme = Package(\n    name: 'flex_color_scheme',\n    description:\n        'A Flutter package to use and make beautiful Material design based themes.',\n    homepage: 'https://docs.flexcolorscheme.com',\n    repository: 'https://github.com/rydmike/flex_color_scheme',\n    authors: [],\n    version: '8.0.2',\n    license: '''BSD 3-Clause License\n\nFlexColorScheme Copyright (c) 2020 - 2024\nMike Rydstrom (Twitter @RydMike GitHub rydmike)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flex_seed_scheme'),\n      PackageRef('flutter'),\n      PackageRef('meta')\n    ]);\n\n/// flex_seed_scheme 3.4.1\nconst _flex_seed_scheme = Package(\n    name: 'flex_seed_scheme',\n    description:\n        \"A more flexible and powerful version of Flutter's ColorScheme.fromSeed. Use multiple seed colors, custom chroma and tone mapping.\",\n    homepage: 'https://github.com/rydmike/flex_seed_scheme',\n    repository: 'https://github.com/rydmike/flex_seed_scheme',\n    authors: [],\n    version: '3.4.1',\n    license: '''BSD 3-Clause License\n\nFlexSeedScheme Copyright (c) 2022-2024\nMike Rydstrom (Twitter @RydMike GitHub rydmike)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('flutter'),\n      PackageRef('meta')\n    ]);\n\n/// flutter 3.24.5\nconst _flutter = Package(\n    name: 'flutter',\n    description: 'A framework for writing Flutter applications',\n    homepage: 'https://flutter.dev',\n    authors: [],\n    version: '3.24.5',\n    license: '''Copyright 2014 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: true,\n    dependencies: [\n      PackageRef('characters'),\n      PackageRef('collection'),\n      PackageRef('material_color_utilities'),\n      PackageRef('meta'),\n      PackageRef('vector_math')\n    ]);\n\n/// flutter_bloc 9.1.1\nconst _flutter_bloc = Package(\n    name: 'flutter_bloc',\n    description:\n        'Flutter widgets that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package.',\n    homepage: 'https://bloclibrary.dev',\n    repository:\n        'https://github.com/felangel/bloc/tree/master/packages/flutter_bloc',\n    authors: [],\n    version: '9.1.1',\n    license: '''The MIT License (MIT)\nCopyright (c) 2025 Felix Angelov\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without restriction,\nincluding without limitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of the Software,\nand to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\nDAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\nOTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE\nUSE OR OTHER DEALINGS IN THE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('bloc'),\n      PackageRef('flutter'),\n      PackageRef('provider')\n    ]);\n\n/// flutter_launcher_icons 0.14.3\nconst _flutter_launcher_icons = Package(\n    name: 'flutter_launcher_icons',\n    description:\n        \"A package which simplifies the task of updating your Flutter app's launcher icon.\",\n    homepage: 'https://github.com/fluttercommunity/flutter_launcher_icons',\n    repository: 'https://github.com/fluttercommunity/flutter_launcher_icons/',\n    authors: [],\n    version: '0.14.3',\n    license: '''MIT License\n\nCopyright (c) 2019 Mark O'Sullivan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('args'),\n      PackageRef('checked_yaml'),\n      PackageRef('cli_util'),\n      PackageRef('image'),\n      PackageRef('json_annotation'),\n      PackageRef('path'),\n      PackageRef('yaml')\n    ]);\n\n/// flutter_lints 5.0.0\nconst _flutter_lints = Package(\n    name: 'flutter_lints',\n    description:\n        'Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/flutter_lints',\n    authors: [],\n    version: '5.0.0',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('lints')]);\n\n/// flutter_oss_licenses 3.0.4\nconst _flutter_oss_licenses = Package(\n    name: 'flutter_oss_licenses',\n    description:\n        'A tool to generate detail and better OSS license list using pubspec.yaml/lock files.',\n    homepage:\n        'https://github.com/espresso3389/flutter_oss_licenses/tree/master/packages/flutter_oss_licenses',\n    repository: 'https://github.com/espresso3389/flutter_oss_licenses',\n    authors: [],\n    version: '3.0.4',\n    license: '''MIT License\n\nCopyright (c) 2019 Takashi Kawasaki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('path'),\n      PackageRef('meta'),\n      PackageRef('yaml'),\n      PackageRef('dart_pubspec_licenses'),\n      PackageRef('args')\n    ]);\n\n/// flutter_plugin_android_lifecycle 2.0.26\nconst _flutter_plugin_android_lifecycle = Package(\n    name: 'flutter_plugin_android_lifecycle',\n    description:\n        'Flutter plugin for accessing an Android Lifecycle within other plugins.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/flutter_plugin_android_lifecycle',\n    authors: [],\n    version: '2.0.26',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('flutter')]);\n\n/// flutter_secure_storage 9.2.4\nconst _flutter_secure_storage = Package(\n    name: 'flutter_secure_storage',\n    description:\n        'Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.',\n    repository:\n        'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',\n    authors: [],\n    version: '9.2.4',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('flutter_secure_storage_linux'),\n      PackageRef('flutter_secure_storage_macos'),\n      PackageRef('flutter_secure_storage_platform_interface'),\n      PackageRef('flutter_secure_storage_web'),\n      PackageRef('flutter_secure_storage_windows'),\n      PackageRef('meta')\n    ]);\n\n/// flutter_secure_storage_linux 1.2.3\nconst _flutter_secure_storage_linux = Package(\n    name: 'flutter_secure_storage_linux',\n    description: 'Linux implementation of flutter_secure_storage',\n    repository: 'https://github.com/mogol/flutter_secure_storage',\n    authors: [],\n    version: '1.2.3',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('flutter_secure_storage_platform_interface')\n    ]);\n\n/// flutter_secure_storage_macos 3.1.3\nconst _flutter_secure_storage_macos = Package(\n    name: 'flutter_secure_storage_macos',\n    description: 'macOS implementation of flutter_secure_storage',\n    repository: 'https://github.com/mogol/flutter_secure_storage',\n    authors: [],\n    version: '3.1.3',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('flutter_secure_storage_platform_interface')\n    ]);\n\n/// flutter_secure_storage_platform_interface 1.1.2\nconst _flutter_secure_storage_platform_interface = Package(\n    name: 'flutter_secure_storage_platform_interface',\n    description:\n        'A common platform interface for the flutter_secure_storage plugin.',\n    homepage: 'https://github.com/mogol/flutter_secure_storage',\n    authors: [],\n    version: '1.1.2',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('plugin_platform_interface')\n    ]);\n\n/// flutter_secure_storage_web 1.2.1\nconst _flutter_secure_storage_web = Package(\n    name: 'flutter_secure_storage_web',\n    description:\n        'Web implementation of flutter_secure_storage. Use flutter_secure_storage for the full flutter package.',\n    repository: 'https://github.com/mogol/flutter_secure_storage',\n    authors: [],\n    version: '1.2.1',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('flutter_secure_storage_platform_interface'),\n      PackageRef('js')\n    ]);\n\n/// flutter_secure_storage_windows 3.1.2\nconst _flutter_secure_storage_windows = Package(\n    name: 'flutter_secure_storage_windows',\n    description:\n        'Windows implementation of flutter_secure_storage. Please use flutter_secure_storage instead of this package.',\n    repository: 'https://github.com/mogol/flutter_secure_storage',\n    authors: [],\n    version: '3.1.2',\n    license: '''BSD 3-Clause License\n\nCopyright 2017 German Saprykin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('ffi'),\n      PackageRef('flutter'),\n      PackageRef('flutter_secure_storage_platform_interface'),\n      PackageRef('path'),\n      PackageRef('path_provider'),\n      PackageRef('win32')\n    ]);\n\n/// go_router 15.1.2\nconst _go_router = Package(\n    name: 'go_router',\n    description:\n        'A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/go_router',\n    authors: [],\n    version: '15.1.2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('flutter'),\n      PackageRef('logging'),\n      PackageRef('meta')\n    ]);\n\n/// http_parser 4.0.2\nconst _http_parser = Package(\n    name: 'http_parser',\n    description:\n        'A platform-independent package for parsing and serializing HTTP formats.',\n    repository: 'https://github.com/dart-lang/http_parser',\n    authors: [],\n    version: '4.0.2',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('source_span'),\n      PackageRef('string_scanner'),\n      PackageRef('typed_data')\n    ]);\n\n/// image 4.5.4\nconst _image = Package(\n    name: 'image',\n    description:\n        'Dart Image Library provides server and web apps the ability to load, manipulate, and save images with various image file formats.',\n    homepage: 'https://github.com/brendan-duncan/image',\n    authors: [],\n    version: '4.5.4',\n    license: '''The MIT License\n\nCopyright (c) 2013-2022 Brendan Duncan.\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('archive'),\n      PackageRef('meta'),\n      PackageRef('xml')\n    ]);\n\n/// intl 0.19.0\nconst _intl = Package(\n    name: 'intl',\n    description:\n        'Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.',\n    repository: 'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',\n    authors: [],\n    version: '0.19.0',\n    license: '''Copyright 2013, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('clock'),\n      PackageRef('meta'),\n      PackageRef('path')\n    ]);\n\n/// js 0.6.7\nconst _js = Package(\n    name: 'js',\n    description:\n        'Annotations to create static Dart interfaces for JavaScript APIs.',\n    repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/js',\n    authors: [],\n    version: '0.6.7',\n    license: '''Copyright 2012, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta')]);\n\n/// json_annotation 4.9.0\nconst _json_annotation = Package(\n    name: 'json_annotation',\n    description:\n        'Classes and helper functions that support JSON code generation via the `json_serializable` package.',\n    repository:\n        'https://github.com/google/json_serializable.dart/tree/master/json_annotation',\n    authors: [],\n    version: '4.9.0',\n    license: '''Copyright 2017, the Dart project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta')]);\n\n/// leak_tracker 10.0.5\nconst _leak_tracker = Package(\n    name: 'leak_tracker',\n    description:\n        'A framework for memory leak tracking for Dart and Flutter applications.',\n    repository:\n        'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker',\n    authors: [],\n    version: '10.0.5',\n    license: '''Copyright 2022, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// leak_tracker_flutter_testing 3.0.5\nconst _leak_tracker_flutter_testing = Package(\n    name: 'leak_tracker_flutter_testing',\n    description: 'An internal package to test leak tracking with Flutter.',\n    repository:\n        'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_flutter_testing',\n    authors: [],\n    version: '3.0.5',\n    license: '''Copyright 2022, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// leak_tracker_testing 3.0.1\nconst _leak_tracker_testing = Package(\n    name: 'leak_tracker_testing',\n    description: 'Leak tracking code intended for usage in tests.',\n    repository:\n        'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_testing',\n    authors: [],\n    version: '3.0.1',\n    license: '''Copyright 2022, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// lints 5.0.0\nconst _lints = Package(\n    name: 'lints',\n    description:\n        \"\"\"Official Dart lint rules. Defines the 'core' and 'recommended' set of lints suggested by the Dart team.\n\"\"\",\n    repository: 'https://github.com/dart-lang/lints',\n    authors: [],\n    version: '5.0.0',\n    license: '''Copyright 2021, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// logging 1.3.0\nconst _logging = Package(\n    name: 'logging',\n    description:\n        'Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger.',\n    repository: 'https://github.com/dart-lang/core/tree/main/pkgs/logging',\n    authors: [],\n    version: '1.3.0',\n    license: '''Copyright 2013, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// matcher 0.12.16+1\nconst _matcher = Package(\n    name: 'matcher',\n    description:\n        'Support for specifying test expectations via an extensible Matcher class. Also includes a number of built-in Matcher implementations for common cases.',\n    repository: 'https://github.com/dart-lang/matcher',\n    authors: [],\n    version: '0.12.16+1',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// material_color_utilities 0.11.1\nconst _material_color_utilities = Package(\n    name: 'material_color_utilities',\n    description:\n        'Algorithms and utilities that power the Material Design 3 color system, including choosing theme colors from images and creating tones of colors; all in a new color space.',\n    repository:\n        'https://github.com/material-foundation/material-color-utilities/tree/main/dart',\n    authors: [],\n    version: '0.11.1',\n    license: '''Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n   To apply the Apache License to your work, attach the following\n   boilerplate notice, with the fields enclosed by brackets \"[]\"\n   replaced with your own identifying information. (Don't include\n   the brackets!)  The text should be enclosed in the appropriate\n   comment syntax for the file format. We also recommend that a\n   file or class name and description of purpose be included on the\n   same \"printed page\" as the copyright notice for easier\n   identification within third-party archives.\n\n   Copyright 2021 Google LLC\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('collection')]);\n\n/// meta 1.15.0\nconst _meta = Package(\n    name: 'meta',\n    description:\n        \"Annotations used to express developer intentions that can't otherwise be deduced by statically analyzing source code.\",\n    repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/meta',\n    authors: [],\n    version: '1.15.0',\n    license: '''Copyright 2016, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// mime 2.0.0\nconst _mime = Package(\n    name: 'mime',\n    description:\n        'Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.',\n    repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/mime',\n    authors: [],\n    version: '2.0.0',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// nested 1.0.0\nconst _nested = Package(\n    name: 'nested',\n    description:\n        'A Flutter Widget which helps nest multiple widgets without needing to manually nest them.',\n    repository: 'https://github.com/rrousselGit/nested',\n    authors: [],\n    version: '1.0.0',\n    license: '''MIT License\n\nCopyright (c) 2019 Remi Rousselet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('flutter')]);\n\n/// path 1.9.0\nconst _path = Package(\n    name: 'path',\n    description:\n        'A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.',\n    repository: 'https://github.com/dart-lang/path',\n    authors: [],\n    version: '1.9.0',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// path_provider 2.1.5\nconst _path_provider = Package(\n    name: 'path_provider',\n    description:\n        'Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider',\n    authors: [],\n    version: '2.1.5',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('path_provider_android'),\n      PackageRef('path_provider_foundation'),\n      PackageRef('path_provider_linux'),\n      PackageRef('path_provider_platform_interface'),\n      PackageRef('path_provider_windows')\n    ]);\n\n/// path_provider_android 2.2.15\nconst _path_provider_android = Package(\n    name: 'path_provider_android',\n    description: 'Android implementation of the path_provider plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_android',\n    authors: [],\n    version: '2.2.15',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('path_provider_platform_interface')\n    ]);\n\n/// path_provider_foundation 2.4.1\nconst _path_provider_foundation = Package(\n    name: 'path_provider_foundation',\n    description: 'iOS and macOS implementation of the path_provider plugin',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_foundation',\n    authors: [],\n    version: '2.4.1',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('path_provider_platform_interface')\n    ]);\n\n/// path_provider_linux 2.2.1\nconst _path_provider_linux = Package(\n    name: 'path_provider_linux',\n    description: 'Linux implementation of the path_provider plugin',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_linux',\n    authors: [],\n    version: '2.2.1',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('ffi'),\n      PackageRef('flutter'),\n      PackageRef('path'),\n      PackageRef('path_provider_platform_interface'),\n      PackageRef('xdg_directories')\n    ]);\n\n/// path_provider_platform_interface 2.1.2\nconst _path_provider_platform_interface = Package(\n    name: 'path_provider_platform_interface',\n    description: 'A common platform interface for the path_provider plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_platform_interface',\n    authors: [],\n    version: '2.1.2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('platform'),\n      PackageRef('plugin_platform_interface')\n    ]);\n\n/// path_provider_windows 2.3.0\nconst _path_provider_windows = Package(\n    name: 'path_provider_windows',\n    description: 'Windows implementation of the path_provider plugin',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_windows',\n    authors: [],\n    version: '2.3.0',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('ffi'),\n      PackageRef('flutter'),\n      PackageRef('path'),\n      PackageRef('path_provider_platform_interface')\n    ]);\n\n/// permission_handler 12.0.0+1\nconst _permission_handler = Package(\n    name: 'permission_handler',\n    description:\n        'Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.',\n    repository: 'https://github.com/baseflow/flutter-permission-handler',\n    authors: [],\n    version: '12.0.0+1',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('meta'),\n      PackageRef('permission_handler_android'),\n      PackageRef('permission_handler_apple'),\n      PackageRef('permission_handler_html'),\n      PackageRef('permission_handler_windows'),\n      PackageRef('permission_handler_platform_interface')\n    ]);\n\n/// permission_handler_android 13.0.1\nconst _permission_handler_android = Package(\n    name: 'permission_handler_android',\n    description:\n        'Permission plugin for Flutter. This plugin provides the Android API to request and check permissions.',\n    homepage: 'https://github.com/baseflow/flutter-permission-handler',\n    authors: [],\n    version: '13.0.1',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('permission_handler_platform_interface')\n    ]);\n\n/// permission_handler_apple 9.4.7\nconst _permission_handler_apple = Package(\n    name: 'permission_handler_apple',\n    description:\n        'Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions.',\n    repository: 'https://github.com/baseflow/flutter-permission-handler',\n    authors: [],\n    version: '9.4.7',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('permission_handler_platform_interface')\n    ]);\n\n/// permission_handler_html 0.1.3+5\nconst _permission_handler_html = Package(\n    name: 'permission_handler_html',\n    description:\n        'Permission plugin for Flutter. This plugin provides the web API to request and check permissions.',\n    homepage: 'https://github.com/baseflow/flutter-permission-handler',\n    authors: [],\n    version: '0.1.3+5',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('permission_handler_platform_interface'),\n      PackageRef('web')\n    ]);\n\n/// permission_handler_platform_interface 4.3.0\nconst _permission_handler_platform_interface = Package(\n    name: 'permission_handler_platform_interface',\n    description:\n        'A common platform interface for the permission_handler plugin.',\n    homepage:\n        'https://github.com/baseflow/flutter-permission-handler/tree/master/permission_handler_platform_interface',\n    authors: [],\n    version: '4.3.0',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('meta'),\n      PackageRef('plugin_platform_interface')\n    ]);\n\n/// permission_handler_windows 0.2.1\nconst _permission_handler_windows = Package(\n    name: 'permission_handler_windows',\n    description:\n        'Permission plugin for Flutter. This plugin provides the Windows API to request and check permissions.',\n    homepage: 'https://github.com/baseflow/flutter-permission-handler',\n    authors: [],\n    version: '0.2.1',\n    license: '''MIT License\n\nCopyright (c) 2018 Baseflow\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('permission_handler_platform_interface')\n    ]);\n\n/// petitparser 6.0.2\nconst _petitparser = Package(\n    name: 'petitparser',\n    description:\n        'A dynamic parser framework to build efficient grammars and parsers quickly.',\n    homepage: 'https://petitparser.github.io',\n    repository: 'https://github.com/petitparser/dart-petitparser',\n    authors: [],\n    version: '6.0.2',\n    license: '''The MIT License\n\nCopyright (c) 2006-2023 Lukas Renggli.\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta')]);\n\n/// platform 3.1.5\nconst _platform = Package(\n    name: 'platform',\n    description:\n        'A pluggable, mockable platform information abstraction for Dart.',\n    repository: 'https://github.com/dart-lang/platform',\n    authors: [],\n    version: '3.1.5',\n    license: '''Copyright 2017, the Dart project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// plugin_platform_interface 2.1.8\nconst _plugin_platform_interface = Package(\n    name: 'plugin_platform_interface',\n    description:\n        'Reusable base class for platform interfaces of Flutter federated plugins, to help enforce best practices.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/plugin_platform_interface',\n    authors: [],\n    version: '2.1.8',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta')]);\n\n/// posix 6.0.2\nconst _posix = Package(\n    name: 'posix',\n    description: 'Exposes the POSIX api on OSx and Linux',\n    homepage: 'https://github.com/onepub-dev/dart_posix',\n    authors: [],\n    version: '6.0.2',\n    license: '''MIT License\n\nCopyright (c) 2020 Brett Sutton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('ffi'), PackageRef('meta'), PackageRef('path')]);\n\n/// process 5.0.2\nconst _process = Package(\n    name: 'process',\n    description:\n        'A pluggable, mockable process invocation abstraction for Dart.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/process',\n    authors: [],\n    version: '5.0.2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// provider 6.1.5\nconst _provider = Package(\n    name: 'provider',\n    description:\n        'A wrapper around InheritedWidget to make them easier to use and more reusable.',\n    repository: 'https://github.com/rrousselGit/provider',\n    authors: [],\n    version: '6.1.5',\n    license: '''MIT License\n\nCopyright (c) 2019 Remi Rousselet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('flutter'),\n      PackageRef('nested')\n    ]);\n\n/// rxdart 0.28.0\nconst _rxdart = Package(\n    name: 'rxdart',\n    description:\n        '''RxDart is an implementation of the popular ReactiveX api for asynchronous programming, leveraging the native Dart Streams api.\n''',\n    repository: 'https://github.com/ReactiveX/rxdart',\n    authors: [],\n    version: '0.28.0',\n    license: '''Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// source_span 1.10.0\nconst _source_span = Package(\n    name: 'source_span',\n    description:\n        'Provides a standard representation for source code locations and spans.',\n    repository: 'https://github.com/dart-lang/source_span',\n    authors: [],\n    version: '1.10.0',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('path'),\n      PackageRef('term_glyph')\n    ]);\n\n/// sqflite_common 2.5.4+6\nconst _sqflite_common = Package(\n    name: 'sqflite_common',\n    description:\n        'Dart wrapper on SQLite, a self-contained, high-reliability, embedded, SQL database engine.',\n    homepage: 'https://github.com/tekartik/sqflite/tree/master/sqflite_common',\n    authors: [],\n    version: '2.5.4+6',\n    license: '''BSD 2-Clause License\n\nCopyright (c) 2019, Alexandre Roux Tekartik\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('synchronized'),\n      PackageRef('path'),\n      PackageRef('meta')\n    ]);\n\n/// sqflite_common_ffi 2.3.4+4\nconst _sqflite_common_ffi = Package(\n    name: 'sqflite_common_ffi',\n    description:\n        'sqflite ffi based implementation, for desktop and units tests.',\n    homepage:\n        'https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi',\n    authors: [],\n    version: '2.3.4+4',\n    license: '''BSD 2-Clause License\n\nCopyright (c) 2019, Alexandre Roux Tekartik\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('sqlite3'),\n      PackageRef('sqflite_common'),\n      PackageRef('synchronized'),\n      PackageRef('path'),\n      PackageRef('meta')\n    ]);\n\n/// sqlite3 2.7.5\nconst _sqlite3 = Package(\n    name: 'sqlite3',\n    description:\n        'Provides lightweight yet convenient bindings to SQLite by using dart:ffi',\n    homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3',\n    authors: [],\n    version: '2.7.5',\n    license: '''MIT License\n\nCopyright (c) 2020 Simon Binder\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('ffi'),\n      PackageRef('meta'),\n      PackageRef('path'),\n      PackageRef('web'),\n      PackageRef('typed_data')\n    ]);\n\n/// sqlite3_flutter_libs 0.5.32\nconst _sqlite3_flutter_libs = Package(\n    name: 'sqlite3_flutter_libs',\n    description:\n        'Flutter plugin to include native sqlite3 libraries with your app',\n    homepage:\n        'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs',\n    authors: [],\n    version: '0.5.32',\n    license: '''MIT License\n\nCopyright (c) 2020 Simon Binder\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('flutter')]);\n\n/// stack_trace 1.11.1\nconst _stack_trace = Package(\n    name: 'stack_trace',\n    description:\n        'A package for manipulating stack traces and printing them readably.',\n    repository: 'https://github.com/dart-lang/stack_trace',\n    authors: [],\n    version: '1.11.1',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// stream_channel 2.1.2\nconst _stream_channel = Package(\n    name: 'stream_channel',\n    description:\n        'An abstraction for two-way communication channels based on the Dart Stream class.',\n    repository: 'https://github.com/dart-lang/stream_channel',\n    authors: [],\n    version: '2.1.2',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// string_scanner 1.2.0\nconst _string_scanner = Package(\n    name: 'string_scanner',\n    description: 'A class for parsing strings using a sequence of patterns.',\n    repository: 'https://github.com/dart-lang/string_scanner',\n    authors: [],\n    version: '1.2.0',\n    license: '''Copyright 2014, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('source_span')]);\n\n/// sync_http 0.3.1\nconst _sync_http = Package(\n    name: 'sync_http',\n    description: 'Synchronous HTTP client for Dart.',\n    repository: 'https://github.com/google/sync_http.dart',\n    authors: [],\n    version: '0.3.1',\n    license: '''Copyright 2017, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// synchronized 3.3.0+3\nconst _synchronized = Package(\n    name: 'synchronized',\n    description:\n        'Lock mechanism to prevent concurrent access to asynchronous code.',\n    homepage:\n        'https://github.com/tekartik/synchronized.dart/tree/master/synchronized',\n    authors: [],\n    version: '3.3.0+3',\n    license: '''MIT License\n\nCopyright (c) 2016, Alexandre Roux Tekartik.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// term_glyph 1.2.1\nconst _term_glyph = Package(\n    name: 'term_glyph',\n    description: 'Useful Unicode glyphs and ASCII substitutes.',\n    repository: 'https://github.com/dart-lang/term_glyph',\n    authors: [],\n    version: '1.2.1',\n    license: '''Copyright 2017, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// test_api 0.7.2\nconst _test_api = Package(\n    name: 'test_api',\n    description:\n        'The user facing API for structuring Dart tests and checking expectations.',\n    repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_api',\n    authors: [],\n    version: '0.7.2',\n    license: '''Copyright 2018, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// test_cov_console 0.2.2\nconst _test_cov_console = Package(\n    name: 'test_cov_console',\n    description:\n        'Command line utility to read lcov.info and print the coverage report to console.',\n    homepage: 'https://github.com/DigitalKatalis/test_cov_console',\n    authors: [],\n    version: '0.2.2',\n    license: '''Copyright (c) 2021, DKatalis.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the <organization> nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// typed_data 1.4.0\nconst _typed_data = Package(\n    name: 'typed_data',\n    description:\n        'Utility functions and classes related to the dart:typed_data library.',\n    repository: 'https://github.com/dart-lang/core/tree/main/pkgs/typed_data',\n    authors: [],\n    version: '1.4.0',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('collection')]);\n\n/// url_launcher 6.3.1\nconst _url_launcher = Package(\n    name: 'url_launcher',\n    description:\n        'Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher',\n    authors: [],\n    version: '6.3.1',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_android'),\n      PackageRef('url_launcher_ios'),\n      PackageRef('url_launcher_linux'),\n      PackageRef('url_launcher_macos'),\n      PackageRef('url_launcher_platform_interface'),\n      PackageRef('url_launcher_web'),\n      PackageRef('url_launcher_windows')\n    ]);\n\n/// url_launcher_android 6.3.14\nconst _url_launcher_android = Package(\n    name: 'url_launcher_android',\n    description: 'Android implementation of the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android',\n    authors: [],\n    version: '6.3.14',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface')\n    ]);\n\n/// url_launcher_ios 6.3.3\nconst _url_launcher_ios = Package(\n    name: 'url_launcher_ios',\n    description: 'iOS implementation of the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios',\n    authors: [],\n    version: '6.3.3',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface')\n    ]);\n\n/// url_launcher_linux 3.2.1\nconst _url_launcher_linux = Package(\n    name: 'url_launcher_linux',\n    description: 'Linux implementation of the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_linux',\n    authors: [],\n    version: '3.2.1',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface')\n    ]);\n\n/// url_launcher_macos 3.2.2\nconst _url_launcher_macos = Package(\n    name: 'url_launcher_macos',\n    description: 'macOS implementation of the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_macos',\n    authors: [],\n    version: '3.2.2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface')\n    ]);\n\n/// url_launcher_platform_interface 2.3.2\nconst _url_launcher_platform_interface = Package(\n    name: 'url_launcher_platform_interface',\n    description: 'A common platform interface for the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_platform_interface',\n    authors: [],\n    version: '2.3.2',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('plugin_platform_interface')\n    ]);\n\n/// url_launcher_web 2.3.3\nconst _url_launcher_web = Package(\n    name: 'url_launcher_web',\n    description: 'Web platform implementation of url_launcher',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web',\n    authors: [],\n    version: '2.3.3',\n    license: '''url_launcher_web\n\nCopyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n--------------------------------------------------------------------------------\nplatform_detect\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2017 Workiva Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface'),\n      PackageRef('web')\n    ]);\n\n/// url_launcher_windows 3.1.4\nconst _url_launcher_windows = Package(\n    name: 'url_launcher_windows',\n    description: 'Windows implementation of the url_launcher plugin.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows',\n    authors: [],\n    version: '3.1.4',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('flutter'),\n      PackageRef('url_launcher_platform_interface')\n    ]);\n\n/// vector_math 2.1.4\nconst _vector_math = Package(\n    name: 'vector_math',\n    description: 'A Vector Math library for 2D and 3D applications.',\n    repository: 'https://github.com/google/vector_math.dart',\n    authors: [],\n    version: '2.1.4',\n    license: '''Copyright 2015, Google Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nCopyright (C) 2013 Andrew Magill\n\nThis software is provided 'as-is', without any express or implied\nwarranty.  In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// vm_service 14.2.5\nconst _vm_service = Package(\n    name: 'vm_service',\n    description:\n        'A library to communicate with a service implementing the Dart VM service protocol.',\n    repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service',\n    authors: [],\n    version: '14.2.5',\n    license: '''Copyright 2015, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// web 1.1.1\nconst _web = Package(\n    name: 'web',\n    description: 'Lightweight browser API bindings built around JS interop.',\n    repository: 'https://github.com/dart-lang/web',\n    authors: [],\n    version: '1.1.1',\n    license: '''Copyright 2023, the Dart project authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google LLC nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// webdav_client 1.2.2\nconst _webdav_client = Package(\n    name: 'webdav_client',\n    description: 'A simple WebDAV client that supports some common methods.',\n    homepage: 'https://github.com/flymzero/webdav_client',\n    authors: [],\n    version: '1.2.2',\n    license: '''BSD 3-Clause License\n\nCopyright (c) 2020, MZERO\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('dio'),\n      PackageRef('xml'),\n      PackageRef('convert')\n    ]);\n\n/// webdriver 3.0.3\nconst _webdriver = Package(\n    name: 'webdriver',\n    description:\n        'Provides WebDriver bindings for Dart. Supports WebDriver JSON interface and W3C spec. Requires the use of WebDriver remote server.',\n    repository: 'https://github.com/google/webdriver.dart',\n    authors: [],\n    version: '3.0.3',\n    license: '''Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2013 Google LLC\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: []);\n\n/// win32 5.10.1\nconst _win32 = Package(\n    name: 'win32',\n    description:\n        '''Access common Win32 APIs directly from Dart using FFI — no C required!\n''',\n    homepage: 'https://win32.pub',\n    repository: 'https://github.com/halildurmus/win32',\n    authors: [],\n    version: '5.10.1',\n    license: '''BSD 3-Clause License\n\nCopyright (c) 2024, Halil Durmus\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('ffi')]);\n\n/// xdg_directories 1.1.0\nconst _xdg_directories = Package(\n    name: 'xdg_directories',\n    description:\n        'A Dart package for reading XDG directory configuration information on Linux.',\n    repository:\n        'https://github.com/flutter/packages/tree/main/packages/xdg_directories',\n    authors: [],\n    version: '1.1.0',\n    license: '''Copyright 2013 The Flutter Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [PackageRef('meta'), PackageRef('path')]);\n\n/// xml 6.5.0\nconst _xml = Package(\n    name: 'xml',\n    description:\n        'A lightweight library for parsing, traversing, querying, transforming and building XML documents.',\n    homepage: 'https://github.com/renggli/dart-xml',\n    authors: [],\n    version: '6.5.0',\n    license: '''The MIT License\n\nCopyright (c) 2006-2023 Lukas Renggli.\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('meta'),\n      PackageRef('petitparser')\n    ]);\n\n/// yaml 3.1.3\nconst _yaml = Package(\n    name: 'yaml',\n    description:\n        'A parser for YAML, a human-friendly data serialization standard',\n    repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/yaml',\n    authors: [],\n    version: '3.1.3',\n    license: '''Copyright (c) 2014, the Dart project authors.\nCopyright (c) 2006, Kirill Simonov.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.''',\n    isMarkdown: false,\n    isSdk: false,\n    dependencies: [\n      PackageRef('collection'),\n      PackageRef('source_span'),\n      PackageRef('string_scanner')\n    ]);\n"
  },
  {
    "path": "lib/setting/controller/fake_setting_controller.dart",
    "content": "// coverage:ignore-file\n\nimport 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingControllerInterface;\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\n\nclass FakeSettingController implements SettingControllerInterface {\n  static final List<Setting> settings = [];\n\n  FakeSettingController();\n\n  @override\n  Future<List<Setting>> list() async => settings;\n\n  @override\n  Future<Setting?> get(dynamic identifier) async {\n    for (Setting s in settings) {\n      if (s.key == identifier) {\n        return s;\n      }\n    }\n    return null;\n  }\n\n  @override\n  Future<int> insert(Setting model) async {\n    settings.add(model);\n    return settings.length;\n  }\n\n  @override\n  Future<Setting> getOrInsert(\n      {required dynamic identifier, required String defaultValue}) async {\n    Setting? result = await get(identifier);\n    if (result == null) {\n      Setting fallback = Setting(key: identifier, value: defaultValue);\n      await insert(fallback);\n      return fallback;\n    } else {\n      return result;\n    }\n  }\n\n  @override\n  Future<int> update(Setting model) async {\n    int index = settings.indexWhere((Setting s) => s.key == model.key);\n    if (index != -1) {\n      settings[index] = model;\n      return index;\n    } else {\n      return 0;\n    }\n  }\n\n  @override\n  Future<int> updateOrInsert(Setting model) async {\n    int id = await update(model);\n    if (id == 0) {\n      id = await insert(model);\n    }\n    return id;\n  }\n\n  @override\n  Future<int> delete(dynamic identifier) async {\n    int index = settings.indexWhere((Setting s) => s.key == identifier);\n    if (index != -1) {\n      settings.removeAt(index);\n      return index;\n    } else {\n      return 0;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/setting/controller/setting_controller.dart",
    "content": "import 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nabstract class SettingControllerInterface\n    implements ModelControllerInterface<Setting> {\n  Future<Setting> getOrInsert(\n      {required dynamic identifier, required String defaultValue});\n\n  Future<int> updateOrInsert(Setting model);\n}\n\nclass SettingController implements SettingControllerInterface {\n  final DatabaseController controller;\n\n  SettingController(this.controller);\n\n  @override\n  Future<List<Setting>> list() async {\n    final Database db = await controller.database;\n    final List<Map<String, dynamic>> maps = await db.query('settings');\n\n    return List.generate(maps.length, (i) {\n      return Setting.fromMap(maps[i]);\n    });\n  }\n\n  @override\n  Future<Setting?> get(dynamic identifier) async {\n    Setting? model;\n    final Database db = await controller.database;\n    final List<Map> maps = await db.query(\n      'settings',\n      columns: ['key', 'value'],\n      where: 'key = ?',\n      whereArgs: [identifier as String],\n    );\n\n    if (maps.isNotEmpty) {\n      model = Setting.fromMap(maps.first);\n    }\n\n    return model;\n  }\n\n  @override\n  Future<int> insert(Setting model) async {\n    final Database db = await controller.database;\n    final int id = await db.insert(\n      'settings',\n      model.toMap(),\n      conflictAlgorithm: ConflictAlgorithm.ignore,\n    );\n\n    return id;\n  }\n\n  @override\n  Future<Setting> getOrInsert(\n      {required dynamic identifier, required String defaultValue}) async {\n    Setting? result = await get(identifier);\n    if (result == null) {\n      Setting fallback = Setting(key: identifier, value: defaultValue);\n      await insert(fallback);\n      return fallback;\n    } else {\n      return result;\n    }\n  }\n\n  @override\n  Future<int> update(Setting model) async {\n    final Database db = await controller.database;\n    final int id = await db.update(\n      'settings',\n      model.toMap(),\n      // Ensure that the model has a matching id.\n      where: 'key = ?',\n      // Pass the models id as a whereArg to prevent SQL injection.\n      whereArgs: [model.key],\n    );\n\n    return id;\n  }\n\n  @override\n  Future<int> updateOrInsert(Setting model) async {\n    int id = await update(model);\n    if (id == 0) {\n      id = await insert(model);\n    }\n    return id;\n  }\n\n  @override\n  Future<int> delete(dynamic identifier) async {\n    final Database db = await controller.database;\n    final int id = await db.delete(\n      'settings',\n      // Ensure that the model has a matching id.\n      where: 'key = ?',\n      // Pass the models id as a whereArg to prevent SQL injection.\n      whereArgs: [identifier],\n    );\n\n    return id;\n  }\n}\n"
  },
  {
    "path": "lib/setting/model/setting_model.dart",
    "content": "import 'package:equatable/equatable.dart';\n\nclass Setting extends Equatable {\n  final String key;\n  final String value;\n\n  const Setting({\n    required this.key,\n    required this.value,\n  });\n\n  Setting.fromMap(Map<dynamic, dynamic> map)\n      : key = map['key'] as String,\n        value = map['value'] as String;\n\n  static String get tableRepr {\n    return '''CREATE TABLE IF NOT EXISTS settings(\n      `id` INTEGER PRIMARY KEY,\n      `key` TEXT NOT NULL UNIQUE,\n      `value` TEXT NOT NULL\n    )''';\n  }\n\n  Setting copyWith({\n    String? key,\n    String? value,\n  }) {\n    return Setting(\n      key: key ?? this.key,\n      value: value ?? this.value,\n    );\n  }\n\n  /// Convert a [Setting] into Map.\n  /// The keys must correspond to the names of the\n  /// columns in the database.\n  Map<String, dynamic> toMap() {\n    return {\n      'key': key,\n      'value': value,\n    };\n  }\n\n  @override\n  String toString() => 'Setting { key: $key value: $value }';\n\n  @override\n  List<Object?> get props => [\n        key,\n        value,\n      ];\n}\n"
  },
  {
    "path": "lib/setting/page/settings_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart' show PopScopeDrawer;\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/common/widget/info_dialog.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/login/state/login_state.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_state.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\n\nclass SettingsPage extends StatelessWidget {\n  const SettingsPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const PopScopeDrawer(\n      child: Scaffold(\n        appBar: MainAppBar(title: 'Settings'),\n        body: SettingsView(),\n      ),\n    );\n  }\n}\n\nclass SettingsView extends StatelessWidget {\n  const SettingsView({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: Text(\n              'Filter',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n        ),\n        const DefaultListOrderSettingsItem(),\n        const DefaultListFilterSettingsItem(),\n        const DefaultListGroupSettingsItem(),\n        const Divider(),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: Text(\n              'Interaction',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n        ),\n        const SwipeLeftActionEnabledSettingsItem(),\n        const SwipeRightActionEnabledSettingsItem(),\n        const Divider(),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: Text(\n              'Storage',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n        ),\n        const TodoFilenameSettingsItem(),\n        const LocalPathSettingsItem(),\n        const RemotePathSettingsItem(),\n        const Divider(),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: Text(\n              'App',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n        ),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Reinitialization'),\n            subtitle: const Text('Reinitialization of the app.'),\n            onTap: () async {\n              final bool confirm = await ConfirmationDialog.dialog(\n                context: context,\n                title: 'Reinitialization',\n                message: 'Do you want to reinitializate the app?',\n                actionLabel: 'Reninitialize',\n                cancelLabel: 'Cancel',\n              );\n              if (context.mounted && confirm) {\n                context.read<DrawerCubit>().reset();\n                await context.read<TodoFileCubit>().resetTodoFileSettings();\n                if (context.mounted) {\n                  await context.read<LoginCubit>().logout();\n                }\n              }\n            },\n          ),\n        ),\n        const Divider(),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: Text(\n              'Others',\n              style: Theme.of(context).textTheme.titleSmall,\n            ),\n          ),\n        ),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('About'),\n            onTap: () => context.pushNamed('app-info'),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass DefaultListOrderSettingsItem extends StatelessWidget {\n  const DefaultListOrderSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) =>\n          previousState.filter.order != state.filter.order,\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Default order'),\n            subtitle: Text(state.filter.order.name),\n            onTap: () async {\n              await DefaultFilterStateOrderDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DefaultListFilterSettingsItem extends StatelessWidget {\n  const DefaultListFilterSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) =>\n          previousState.filter.filter != state.filter.filter,\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Default filter'),\n            subtitle: Text(state.filter.filter.name),\n            onTap: () async {\n              await DefaultFilterStateFilterDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DefaultListGroupSettingsItem extends StatelessWidget {\n  const DefaultListGroupSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      buildWhen: (FilterState previousState, FilterState state) =>\n          previousState.filter.group != state.filter.group,\n      builder: (BuildContext context, FilterState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Default grouping'),\n            subtitle: Text(state.filter.group.name),\n            onTap: () async {\n              await DefaultFilterStateGroupDialog.dialog(\n                context: context,\n                cubit: BlocProvider.of<FilterCubit>(context),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SwipeLeftActionEnabledSettingsItem extends StatelessWidget {\n  const SwipeLeftActionEnabledSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<InteractionSettingsCubit, InteractionSettingsState>(\n      buildWhen: (InteractionSettingsState previousState,\n              InteractionSettingsState state) =>\n          previousState.swipeLeftActionEnabled != state.swipeLeftActionEnabled,\n      builder: (BuildContext context, InteractionSettingsState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: CheckboxListTile(\n            title: const Text('Enable swipe left action'),\n            value: state.swipeLeftActionEnabled,\n            subtitle: const Text(\n              'If enabled, you can swipe left on a item to perform an action.',\n            ),\n            isThreeLine: true,\n            onChanged: (bool? value) {\n              BlocProvider.of<InteractionSettingsCubit>(context)\n                  .setSwipeLeftActionEnabled(value ?? false);\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SwipeRightActionEnabledSettingsItem extends StatelessWidget {\n  const SwipeRightActionEnabledSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<InteractionSettingsCubit, InteractionSettingsState>(\n      buildWhen: (InteractionSettingsState previousState,\n              InteractionSettingsState state) =>\n          previousState.swipeRightActionEnabled !=\n          state.swipeRightActionEnabled,\n      builder: (BuildContext context, InteractionSettingsState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: CheckboxListTile(\n            title: const Text('Enable swipe right action'),\n            value: state.swipeRightActionEnabled,\n            subtitle: const Text(\n              'If enabled, you can swipe right on a item to perform an action.',\n            ),\n            isThreeLine: true,\n            onChanged: (bool? value) {\n              BlocProvider.of<InteractionSettingsCubit>(context)\n                  .setSwipeRightActionEnabled(value ?? false);\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass LocalPathSettingsItem extends StatelessWidget {\n  const LocalPathSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      buildWhen: (TodoFileState previousState, TodoFileState state) =>\n          previousState.localPath != state.localPath,\n      builder: (BuildContext context, TodoFileState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Local path'),\n            subtitle: Text(state.localPath),\n            onTap: () => InfoDialog.dialog(\n              context: context,\n              title: 'Local path',\n              message:\n                  'Changing this value after initializing the app is not supported.\\n\\nIf you want to change this value, you must reinitialize the app.',\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass RemotePathSettingsItem extends StatelessWidget {\n  const RemotePathSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<LoginCubit, LoginState>(\n      builder: (BuildContext context, LoginState loginState) {\n        return Visibility(\n          visible: loginState is LoginWebDAV,\n          child: BlocBuilder<TodoFileCubit, TodoFileState>(\n            buildWhen: (TodoFileState previousTodoFileState,\n                    TodoFileState todoFileState) =>\n                previousTodoFileState.remotePath != todoFileState.remotePath,\n            builder: (BuildContext context, TodoFileState todoFileState) {\n              return Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                child: ListTile(\n                  title: const Text('Remote path'),\n                  subtitle: Text(todoFileState.remotePath),\n                  onTap: () => InfoDialog.dialog(\n                    context: context,\n                    title: 'Remote path',\n                    message:\n                        'Changing this value after initializing the app is not supported.\\n\\nIf you want to change this value, you must reinitialize the app.',\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoFilenameSettingsItem extends StatelessWidget {\n  const TodoFilenameSettingsItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoFileCubit, TodoFileState>(\n      buildWhen: (TodoFileState previousState, TodoFileState state) =>\n          previousState.todoFilename != state.todoFilename,\n      builder: (BuildContext context, TodoFileState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            title: const Text('Todo filename'),\n            subtitle: Text(state.todoFilename),\n            onTap: () => InfoDialog.dialog(\n              context: context,\n              title: 'Todo filename',\n              message:\n                  'Changing this value after initializing the app is not supported.\\n\\nIf you want to change this value, you must reinitialize the app.',\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/setting/repository/setting_repository.dart",
    "content": "import 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingControllerInterface;\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\n\nclass SettingRepository {\n  final SettingControllerInterface controller;\n\n  SettingRepository(this.controller);\n\n  Future<List<Setting>> list() async => await controller.list();\n\n  Future<Setting?> get({required String key}) async =>\n      await controller.get(key);\n\n  Future<Setting> getOrInsert(\n          {required String key, required String defaultValue}) async =>\n      await controller.getOrInsert(identifier: key, defaultValue: defaultValue);\n\n  Future<int> insert(Setting model) async => await controller.insert(model);\n\n  Future<int> update(Setting model) async => await controller.update(model);\n\n  Future<int> delete({required String key}) async =>\n      await controller.delete(key);\n\n  Future<int> updateOrInsert(Setting model) async =>\n      await controller.updateOrInsert(model);\n}\n"
  },
  {
    "path": "lib/setting/state/interaction_settings_cubit.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/setting/model/setting_model.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_state.dart';\n\nclass InteractionSettingsCubit extends Cubit<InteractionSettingsState> {\n  final SettingRepository repository;\n\n  InteractionSettingsCubit({\n    required this.repository,\n  }) : super(\n          InteractionSettingsLoading(\n            swipeLeftActionEnabled: false,\n            swipeRightActionEnabled: false,\n          ),\n        );\n\n  bool parseBoolOrFalse(String? value) {\n    return value?.trim().toLowerCase() == 'true';\n  }\n\n  Future<void> load() async {\n    try {\n      if (state is InteractionSettingsLoading) {\n        emit(\n          state.save(\n            swipeLeftActionEnabled: parseBoolOrFalse(\n                (await repository.get(key: 'swipeLeftActionEnabled'))?.value),\n            swipeRightActionEnabled: parseBoolOrFalse(\n                (await repository.get(key: 'swipeRightActionEnabled'))?.value),\n          ),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> setSwipeLeftActionEnabled(bool value) async {\n    try {\n      emit(state.save(swipeLeftActionEnabled: value));\n      await repository.updateOrInsert(\n        Setting(key: 'swipeLeftActionEnabled', value: value.toString()),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> setSwipeRightActionEnabled(bool value) async {\n    try {\n      emit(state.save(swipeRightActionEnabled: value));\n      await repository.updateOrInsert(\n        Setting(key: 'swipeRightActionEnabled', value: value.toString()),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/setting/state/interaction_settings_state.dart",
    "content": "import 'package:equatable/equatable.dart';\n\nsealed class InteractionSettingsState extends Equatable {\n  final bool swipeLeftActionEnabled;\n  final bool swipeRightActionEnabled;\n\n  const InteractionSettingsState({\n    required this.swipeLeftActionEnabled,\n    required this.swipeRightActionEnabled,\n  });\n\n  InteractionSettingsLoading loading({\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) {\n    return InteractionSettingsLoading(\n      swipeLeftActionEnabled:\n          swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n      swipeRightActionEnabled:\n          swipeRightActionEnabled ?? this.swipeRightActionEnabled,\n    );\n  }\n\n  InteractionSettingsSaved save({\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) {\n    return InteractionSettingsSaved(\n      swipeLeftActionEnabled:\n          swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n      swipeRightActionEnabled:\n          swipeRightActionEnabled ?? this.swipeRightActionEnabled,\n    );\n  }\n\n  InteractionSettingsError error({\n    required String message,\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) {\n    return InteractionSettingsError(\n      message: message,\n      swipeLeftActionEnabled:\n          swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n      swipeRightActionEnabled:\n          swipeRightActionEnabled ?? this.swipeRightActionEnabled,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        swipeLeftActionEnabled,\n        swipeRightActionEnabled,\n      ];\n\n  @override\n  String toString() =>\n      'InteractionSettingsState { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }';\n}\n\nfinal class InteractionSettingsLoading extends InteractionSettingsState {\n  const InteractionSettingsLoading({\n    required super.swipeLeftActionEnabled,\n    required super.swipeRightActionEnabled,\n  });\n\n  InteractionSettingsLoading copyWith({\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) =>\n      super.loading(\n        swipeLeftActionEnabled:\n            swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n        swipeRightActionEnabled:\n            swipeRightActionEnabled ?? this.swipeRightActionEnabled,\n      );\n\n  @override\n  String toString() =>\n      'InteractionSettingsLoading { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }';\n}\n\nfinal class InteractionSettingsSaved extends InteractionSettingsState {\n  const InteractionSettingsSaved({\n    required super.swipeLeftActionEnabled,\n    required super.swipeRightActionEnabled,\n  });\n\n  InteractionSettingsSaved copyWith({\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) =>\n      super.save(\n        swipeLeftActionEnabled:\n            swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n        swipeRightActionEnabled:\n            swipeRightActionEnabled ?? this.swipeRightActionEnabled,\n      );\n\n  @override\n  String toString() =>\n      'InteractionSettingsSaved { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }';\n}\n\nfinal class InteractionSettingsError extends InteractionSettingsState {\n  final String message;\n\n  const InteractionSettingsError({\n    required this.message,\n    required super.swipeLeftActionEnabled,\n    required super.swipeRightActionEnabled,\n  });\n\n  InteractionSettingsError copyWith({\n    String? message,\n    bool? swipeLeftActionEnabled,\n    bool? swipeRightActionEnabled,\n  }) =>\n      super.error(\n          message: message ?? this.message,\n          swipeLeftActionEnabled:\n              swipeLeftActionEnabled ?? this.swipeLeftActionEnabled,\n          swipeRightActionEnabled:\n              swipeRightActionEnabled ?? this.swipeRightActionEnabled);\n\n  @override\n  List<Object?> get props => [\n        message,\n        swipeLeftActionEnabled,\n        swipeRightActionEnabled,\n      ];\n\n  @override\n  String toString() =>\n      'InteractionSettingsError { message: $message, swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }';\n}\n"
  },
  {
    "path": "lib/todo/api/todo_list_api.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:collection/collection.dart';\nimport 'package:ntodotxt/main.dart' show log;\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\nimport 'package:rxdart/subjects.dart';\nimport 'package:webdav_client/webdav_client.dart' as webdav;\n\nclass LocalFile {\n  final File file;\n\n  LocalFile(String path)\n      : file = File(path.replaceAllMapped(RegExp(r'\\/{2,}'), (match) => '/'));\n\n  LocalFile.fromFile(this.file);\n\n  String get path => file.uri.pathSegments.last;\n\n  Future<DateTime> get lastModified async => await file.lastModified();\n}\n\nclass WebDAVFile {\n  final String path;\n  final WebDAVClient client;\n\n  WebDAVFile(String path, this.client)\n      : path = path.replaceAllMapped(RegExp(r'\\/{2,}'), (match) => '/');\n\n  Future<webdav.File> get file async => await client.getFile(filename: path);\n\n  Future<DateTime?> get lastModified async => (await file).mTime;\n}\n\nabstract class TodoListApi {\n  /// Provides a [Stream] of all todos read from the source.\n  Stream<List<Todo>> getTodoList();\n\n  Future<void> initSource();\n\n  /// Read [todoList] from source.\n  Future<void> readFromSource();\n\n  /// Write [todoList] to source.\n  Future<void> writeToSource();\n\n  bool existsTodo(Todo todo);\n\n  /// Saves a [todo].\n  /// If a [todo] with [id] already exists, it will be replaced.\n  /// If the [todo] with [id] already exists it will be updated/merged.\n  void saveTodo(Todo todo);\n\n  /// Saves multiple [todos] by [id] at once.\n  void saveMultipleTodos(List<Todo> todos);\n\n  /// Deletes the given [todo] by [id].\n  void deleteTodo(Todo todo);\n\n  /// Deletes multiple [todos] by [id] at once.\n  void deleteMultipleTodos(List<Todo> todos);\n}\n\nclass LocalTodoListApi extends TodoListApi {\n  final LocalFile localFile;\n\n  LocalTodoListApi(this.localFile) {\n    if (localFile.file.existsSync() == false) {\n      log.fine('File ${localFile.path} does not exist. Creating.');\n      localFile.file.createSync();\n    } else {\n      log.fine('File ${localFile.path} exists already.');\n    }\n    updateList(readSync()); // Read synchrone here.\n  }\n\n  LocalTodoListApi.fromString({\n    required String localFilePath,\n  }) : this(LocalFile(localFilePath));\n\n  LocalTodoListApi.fromFile({\n    required File localFile,\n  }) : this(LocalFile.fromFile(localFile));\n\n  /// Provides a [Stream] of all todos.\n  // A special Streamcontroller that captures the latest item that has been\n  // added to the controller, and emits that as the first item to any new listener.\n  final BehaviorSubject<List<Todo>> controller =\n      BehaviorSubject<List<Todo>>.seeded(const []);\n\n  List<Todo> get _todoList => controller.value;\n\n  void updateList(List<Todo> todoList) {\n    // Update only if list does'nt match to prevent weird state changes.\n    if (const ListEquality().equals(_todoList, todoList) == false) {\n      log.fine('Update todo list.');\n      _dispatch(todoList);\n    } else {\n      log.fine('Skip update todo list. List matches with the previous one.');\n    }\n  }\n\n  void _dispatch(List<Todo> todoList) {\n    controller.add(todoList);\n    log.finest(\n      'Updated todos ${[for (var todo in _todoList) todo]}',\n    );\n  }\n\n  void dispose() {\n    controller.close();\n  }\n\n  List<Todo> _read(List<String> rawTodoList) {\n    return [\n      for (var t in rawTodoList)\n        if (t.isNotEmpty) Todo.fromString(value: t)\n    ];\n  }\n\n  Future<List<Todo>> read() async {\n    log.info('Async-read todos from file');\n    return _read(await localFile.file.readAsLines());\n  }\n\n  List<Todo> readSync() {\n    log.info('Sync-read todos from file');\n    return _read(localFile.file.readAsLinesSync());\n  }\n\n  Future<void> write(String content) async {\n    log.info('Sync-write todos to file');\n    await localFile.file.writeAsString(content);\n  }\n\n  @override\n  Stream<List<Todo>> getTodoList() => controller.asBroadcastStream();\n\n  @override\n  Future<void> initSource() async {\n    log.info('Initialize todo file');\n    if (await localFile.file.exists() == false) {\n      await localFile.file.create();\n    }\n  }\n\n  @override\n  Future<void> readFromSource() async => updateList(await read());\n\n  @override\n  Future<void> writeToSource() async => write(\n        _todoList.join(Platform.lineTerminator),\n      );\n\n  @override\n  bool existsTodo(Todo todo) =>\n      _todoList.indexWhere((t) => t.id == todo.id) == -1 ? false : true;\n\n  List<Todo> _save(List<Todo> todoList, Todo todo) {\n    int index = todoList.indexWhere((t) => t.id == todo.id);\n    if (index == -1) {\n      // If not exist save the todo.\n      log.info('Create new todo');\n      todoList.add(todo.copyWith());\n    } else {\n      // If exist update todo and merge changes only.\n      log.info('Update existing todo');\n      todoList[index] = todo.copyMerge(todoList[index]);\n    }\n\n    return todoList;\n  }\n\n  @override\n  void saveTodo(Todo todo) {\n    log.info('Save todo ${todo.id}');\n    List<Todo> todoList = [..._todoList];\n    updateList(_save(todoList, todo));\n  }\n\n  @override\n  void saveMultipleTodos(List<Todo> todos) {\n    log.info('Save todos ${[for (var t in todos) t.id]}');\n    List<Todo> todoList = [..._todoList];\n    for (var todo in todos) {\n      todoList = _save(todoList, todo);\n    }\n    updateList(todoList);\n  }\n\n  List<Todo> _delete(List<Todo> todoList, Todo todo) {\n    todoList.removeWhere((t) => t.id == todo.id);\n    return todoList;\n  }\n\n  @override\n  void deleteTodo(Todo todo) {\n    log.info('Delete todo ${todo.id}');\n    List<Todo> todoList = [..._todoList];\n    updateList(_delete(todoList, todo));\n  }\n\n  @override\n  void deleteMultipleTodos(List<Todo> todos) {\n    log.info('Delete todos ${[for (var t in todos) t.id]}');\n    List<Todo> todoList = [..._todoList];\n    for (var todo in todos) {\n      todoList = _delete(todoList, todo);\n    }\n    updateList(todoList);\n  }\n}\n\nclass WebDAVTodoListApi extends LocalTodoListApi {\n  final WebDAVFile remoteFile;\n  final WebDAVClient client;\n\n  WebDAVTodoListApi(\n    super.localFile,\n    this.remoteFile,\n    this.client,\n  );\n\n  WebDAVTodoListApi.fromString({\n    required String localFilePath,\n    required String remoteFilePath,\n    required WebDAVClient client,\n  }) : this(\n          LocalFile(localFilePath),\n          WebDAVFile(remoteFilePath, client),\n          client,\n        );\n\n  @override\n  Future<void> initSource() async {\n    await super.initSource();\n    await client.ping();\n    if (await client.fileExists(filename: remoteFile.path)) {\n      await readFromSource();\n    } else {\n      await writeToSource();\n    }\n  }\n\n  @override\n  Future<void> readFromSource() async {\n    await write(await downloadFromSource());\n    await super.readFromSource();\n  }\n\n  @override\n  Future<void> writeToSource() async {\n    await super.writeToSource();\n    await uploadToSource();\n  }\n\n  Future<String> downloadFromSource() async {\n    log.info('Download todos from server');\n    return await client.download(filename: remoteFile.path);\n  }\n\n  Future<void> uploadToSource() async {\n    log.info('Upload todos to server');\n    await client.upload(\n      filename: remoteFile.path,\n      content: _todoList.join(Platform.lineTerminator),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/todo/model/todo_model.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/common/exception/exceptions.dart';\n\nenum Priority {\n  none,\n  A,\n  B,\n  C,\n  D,\n  E,\n  F,\n  G,\n  H,\n  I,\n  J,\n  K,\n  L,\n  M,\n  N,\n  O,\n  P,\n  Q,\n  R,\n  S,\n  T,\n  U,\n  V,\n  W,\n  X,\n  Y,\n  Z\n}\n\nextension Priorities on Priority {\n  static Set<Priority> get priorities => {\n        for (var p in Priority.values)\n          if (p != Priority.none) p,\n      };\n\n  static Set<String> get priorityNames => {\n        for (var p in Priority.values)\n          if (p != Priority.none) p.name,\n      };\n\n  static Priority byName(String name) {\n    try {\n      return Priority.values.byName(name);\n    } on Exception {\n      // Returns Priortity.none\n    }\n\n    return Priority.none;\n  }\n\n  static Set<Priority> sort(Set<Priority> priorities) {\n    List<Priority> sorted = priorities.toList()\n      ..sort(\n        (Priority a, Priority b) => a.index.compareTo(b.index),\n      );\n    return sorted.toSet();\n  }\n}\n\n///\n/// Structure of a valid todo string\n/// with some modification (https://github.com/todotxt/todo.txt/discussions/52)\n///\n/// [completion]      (optional)\n/// [completionDate]  (forbidden if incompleted, mandatory if completed)\n/// [priority]        (optional)\n/// [creationDate]    (optional)\n/// [\n///\n///   [fullDescription] (description + tags: projects, context, keyValues\n///                      can be placed anywhere here)\n///   [description]     (mandatory)\n///   [projects]        (optional: preceded by a single space and a '+',\n///                      contains any non-whitespace character)\n///   [contexts]        (optional: preceded by a single space and a '@',\n///                      contains any non-whitespace character)\n///   [keyValues]       (optional: separated by a single colon,\n///                      key value contains any non-whitespace character which are not colons)\n/// ]\n///\n/// VALID examples:\n///\n/// Write some tests                        (When incomplete, no date is required)\n/// 2019-07-01 Write some tests             (The provided date is the creation date)\n/// x 2019-07-03 Write some test            (The provided date is the completion date)\n/// x 2019-07-03 2019-07-01 Write some test (The provided dates are, in order, completion then creation)\n///\n/// INVALID examples:\n///\n/// 2019-07-03 2019-07-01 Write some tests  (The task is incomplete, so can't have a completion date)\n/// x Write some tests                      (A completed task needs at least a completion date)\n///\nclass Todo extends Equatable {\n  static final RegExp patternWord = RegExp(r'^\\S+$');\n  // Limit priorities from A-F.\n  static final RegExp patternPriority = RegExp(r'^\\((?<priority>[A-Z])\\)$');\n  static final RegExp patternDate = RegExp(r'^\\d{4}-\\d{2}-\\d{2}$');\n  static final RegExp patternProject = RegExp(r'^\\+\\S+$');\n  static final RegExp patternContext = RegExp(r'^\\@\\S+$');\n  static final RegExp patternKeyValue = RegExp(r'^([\\w\\-\\.]+):([^\\s:]+)$');\n\n  final String id;\n\n  /// Whether the [Todo] is completed.\n  /// Defaults to null (unset).\n  final bool? _completion;\n\n  /// The priority of the [Todo].\n  /// Priorities are A, B, C, ...\n  /// Defaults to null (empty, unset).\n  final Priority? _priority;\n\n  /// The completion date of the [Todo].\n  /// Defaults to null (unset).\n  final DateTime? _completionDate;\n\n  /// The creation date of the [Todo].\n  /// Defaults to null (unser).\n  final DateTime? _creationDate;\n\n  /// The description of the [Todo].\n  /// Defaults to null (unset).\n  final String? _description;\n\n  /// Whether the [Todo] is completed.\n  /// Defaults to false.\n  bool get completion => _completion ?? false;\n\n  String get fmtCompletion => completion ? 'x' : '';\n\n  /// The priority of the [Todo].\n  /// Allowed values are A, B, C, ... or null (no empty string).\n  /// Empty string value is used to reset the priority.\n  Priority get priority => _priority ?? Priority.none;\n\n  String get fmtPriority {\n    if (priority == Priority.none) {\n      return '';\n    }\n    return '(${priority.name})';\n  }\n\n  /// The completion date of the [Todo].\n  /// Defaults to null.\n  DateTime? get completionDate => _completionDate;\n\n  String get fmtCompletionDate => date2Str(completionDate) ?? '';\n\n  /// The creation date of the [Todo].\n  /// Defaults to null.\n  DateTime? get creationDate => _creationDate;\n\n  String get fmtCreationDate => date2Str(creationDate) ?? '';\n\n  /// The description of the [Todo].\n  /// Returns the description or an empty string (if null).\n  String get description => _description ?? '';\n\n  String get fmtDescription {\n    final List<String> descriptionList = [];\n    for (String item in description.split(' ')) {\n      if (matchProject(item)) continue;\n      if (matchContext(item)) continue;\n      if (matchKeyValue(item)) continue;\n      descriptionList.add(item);\n    }\n\n    return descriptionList.join(' ');\n  }\n\n  /// The list of contexts of the [Todo].\n  /// Defaults to an empty [Set].\n  Set<String> get projects {\n    List<String> projects = [];\n    for (String item in description.split(' ')) {\n      if (matchProject(item)) {\n        projects.add(item.substring(1));\n      }\n    }\n\n    return (projects..sort()).toSet();\n  }\n\n  Set<String> get fmtProjects => {for (var p in projects) '+$p'};\n\n  static fmtProject(String p) => p.startsWith('+') ? p : '+$p';\n\n  bool containsProject(String project) {\n    if (project.startsWith('+')) {\n      return projects.contains(project.substring(1));\n    } else {\n      return projects.contains(project);\n    }\n  }\n\n  static bool matchProject(String project) => patternProject.hasMatch(project);\n\n  /// The list of contexts of the [Todo].\n  /// Defaults to an empty [Set].\n  Set<String> get contexts {\n    List<String> contexts = [];\n    for (String item in description.split(' ')) {\n      if (matchContext(item)) {\n        contexts.add(item.substring(1));\n      }\n    }\n\n    return (contexts..sort()).toSet();\n  }\n\n  Set<String> get fmtContexts => {for (var c in contexts) '@$c'};\n\n  static fmtContext(String c) => c.startsWith('@') ? c : '@$c';\n\n  bool containsContext(String context) {\n    if (context.startsWith('@')) {\n      return contexts.contains(context.substring(1));\n    } else {\n      return contexts.contains(context);\n    }\n  }\n\n  static bool matchContext(String context) => patternContext.hasMatch(context);\n\n  /// The list of key value pairs of the [Todo].\n  /// Defaults to an empty [Map].\n  Set<String> get keyValues {\n    List<String> keyValues = [];\n    for (String item in description.split(' ')) {\n      if (matchKeyValue(item)) {\n        List<String> kvSplitted = item.split(':');\n        if (kvSplitted.length > 2) continue;\n        keyValues.add(item);\n      }\n    }\n\n    return (keyValues..sort()).toSet();\n  }\n\n  Set<String> get fmtKeyValues => keyValues;\n\n  static fmtKeyValue(String keyValue) => keyValue;\n\n  /// Checks if a key value pair with specific key already exists.\n  bool containsKeyValue(String keyValue) {\n    for (String kv in keyValues) {\n      if (kv.split(':')[0] == keyValue.split(':')[0]) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  static bool matchKeyValue(String kv) => patternKeyValue.hasMatch(kv);\n\n  DateTime? get dueDate {\n    for (String kv in keyValues) {\n      List<String> kvSplitted = kv.split(':');\n      if (kvSplitted[0] == 'due') {\n        return str2date(kvSplitted[1]);\n      }\n    }\n\n    return null;\n  }\n\n  String get fmtDueDate {\n    final String? dueDateStr = date2Str(dueDate);\n    return dueDateStr != null ? 'due:$dueDateStr' : '';\n  }\n\n  // Core todo constructor with validation logic.\n  Todo._({\n    required this.id,\n    bool? completion,\n    Priority? priority,\n    DateTime? completionDate,\n    DateTime? creationDate,\n    String? description,\n  })  : _completion = completion,\n        _priority = priority,\n        _completionDate = completionDate,\n        _creationDate = creationDate,\n        _description = description {\n    // Validate completion date.\n    if (completion == true) {\n      if (completionDate == null) {\n        // A completed todo needs at least a completion date.\n        throw const TodoMissingCompletionDate();\n      }\n    } else {\n      if (completionDate != null) {\n        // A completed todo cannot have a completion date.\n        throw const TodoForbiddenCompletionDate();\n      }\n    }\n  }\n\n  /// Factory for model creation with safety mechanisms.\n  factory Todo({\n    String? id,\n    bool? completion,\n    Priority? priority,\n    DateTime? completionDate,\n    DateTime? creationDate,\n    String? description,\n  }) {\n    final DateTime now = DateTime.now();\n\n    if (completion == true) {\n      if (completionDate == null) {\n        completionDate = DateTime(now.year, now.month, now.day);\n      } else {\n        completionDate = DateTime(\n            completionDate.year, completionDate.month, completionDate.day);\n      }\n    } else {\n      completionDate = null;\n    }\n\n    if (creationDate == null) {\n      // Initialize creationDate to be sure there is always one set.\n      creationDate = DateTime(now.year, now.month, now.day);\n    } else {\n      creationDate =\n          DateTime(creationDate.year, creationDate.month, creationDate.day);\n    }\n\n    return Todo._(\n      id: id ?? Todo.genId(),\n      completion: completion,\n      priority: priority,\n      completionDate: completionDate,\n      creationDate: creationDate,\n      description: description != null\n          ? description.replaceAllMapped(RegExp(r'\\s{2,}'), (match) => ' ')\n          : description,\n    );\n  }\n\n  factory Todo.fromString({\n    String? id,\n    required String value,\n  }) {\n    final todoStr =\n        value.trim().replaceAllMapped(RegExp(r'\\s{2,}'), (match) => ' ');\n\n    bool completion = _str2completion(\n      _todoStringElementAt(todoStr, 0),\n    );\n    Priority priority;\n    DateTime? completionDate;\n    DateTime? creationDate;\n    List<String>? fullDescriptionList;\n\n    // Get completion\n    if (completion) {\n      completionDate = str2date(\n        _todoStringElementAt(todoStr, 1),\n      );\n      priority = _str2priority(\n        _todoStringElementAt(todoStr, 2),\n      );\n      // x <completionDate> [<priority>] [<creationDate>] <fullDescription>\n      if (priority == Priority.none) {\n        creationDate = str2date(\n          _todoStringElementAt(todoStr, 2),\n        );\n      } else {\n        creationDate = str2date(\n          _todoStringElementAt(todoStr, 3),\n        );\n      }\n    } else {\n      priority = _str2priority(\n        _todoStringElementAt(todoStr, 0),\n      );\n      // [<priority>] [<creationDate>] <fullDescription>\n      if (priority == Priority.none) {\n        // The provided date is the creation date (todo incompleted).\n        creationDate = str2date(\n          _todoStringElementAt(todoStr, 0),\n        );\n      } else {\n        // The provided date is the creation date (todo incompleted).\n        creationDate = str2date(\n          _todoStringElementAt(todoStr, 1),\n        );\n      }\n      // The todo is not completed so two dates are forbidden.\n      // Everything that comes after the creationDate is interpreted as the description.\n    }\n\n    // Get beginning of description.\n    int descriptionIndex = 0;\n    for (var prop in [completion, completionDate, priority, creationDate]) {\n      if (prop != null && prop != false && prop != Priority.none) {\n        descriptionIndex += 1;\n      }\n    }\n    try {\n      fullDescriptionList = todoStr.split(' ').sublist(descriptionIndex);\n    } on RangeError {\n      fullDescriptionList = [];\n    }\n\n    return Todo(\n      id: id ?? Todo.genId(),\n      completion: completion,\n      priority: priority,\n      completionDate: completionDate,\n      creationDate: creationDate,\n      description: fullDescriptionList.join(' '), // Including tags.\n    );\n  }\n\n  /// A regular copyWith function.\n  Todo copyWith({\n    bool? completion,\n    Priority? priority,\n    DateTime? completionDate,\n    DateTime? creationDate,\n    String? description,\n  }) {\n    return Todo(\n      id: id,\n      completion: completion ?? this.completion,\n      priority: priority ?? this.priority,\n      completionDate: completionDate ?? this.completionDate,\n      creationDate: creationDate ?? this.creationDate,\n      description: description ?? this.description,\n    );\n  }\n\n  /// Creates a todo object that only sets the values\n  /// that have been explicitly edited.\n  /// The other values remain to null.\n  Todo copyDiff({\n    bool? completion,\n    Priority? priority,\n    DateTime? completionDate,\n    DateTime? creationDate,\n    String? description,\n  }) {\n    return Todo(\n      id: id,\n      completion: completion,\n      priority: priority,\n      completionDate: completionDate,\n      // Once the creationDate is set, keep it.\n      creationDate: creationDate ?? this.creationDate,\n      description: description,\n    );\n  }\n\n  /// Copy only the explicitly set attributes into the new object.\n  /// Use the existing values for the rest.\n  /// If the values of _<variables> are not null, they have been\n  /// explicitly edited.\n  Todo copyMerge(Todo todo) {\n    return Todo(\n      id: id,\n      completion: _completion ?? todo.completion,\n      priority: _priority ?? todo.priority,\n      completionDate: _completionDate ?? todo.completionDate,\n      creationDate: _creationDate ?? todo.creationDate,\n      description: _description ?? todo.description,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        completion,\n        completionDate,\n        priority,\n        creationDate,\n        description,\n      ];\n\n  @override\n  String toString() {\n    final List<String> items = [\n      fmtCompletion,\n      fmtCompletionDate,\n      fmtPriority,\n      fmtCreationDate,\n      description,\n    ]..removeWhere((value) => value.isEmpty);\n\n    return items.join(' ');\n  }\n\n  static String genId({int len = 32}) {\n    final Random r = Random();\n    const chars =\n        'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';\n    final String randomId =\n        List.generate(len, (index) => chars[r.nextInt(chars.length)]).join();\n    return sha256.convert(utf8.encode(randomId)).toString();\n  }\n\n  static DateTime? str2date(String value) {\n    if (patternDate.hasMatch(value)) {\n      return DateTime.parse(value);\n    } else {\n      return null;\n    }\n  }\n\n  static String? date2Str(DateTime? date) {\n    if (date == null) {\n      return null;\n    }\n    return \"${date.year.toString()}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}\";\n  }\n\n  static int compareToToday(DateTime date) {\n    final DateTime now = DateTime.now();\n    final DateTime today = DateTime(now.year, now.month, now.day);\n    return date.compareTo(today);\n  }\n\n  static String differenceToToday(DateTime date) {\n    final DateTime now = DateTime.now();\n    final DateTime today = DateTime(now.year, now.month, now.day);\n    final Duration difference = today.difference(date);\n    final int days = difference.inDays;\n\n    if (days < 0) {\n      return 'In future'; // In real this should never the case.\n    } else if (days == 0) {\n      return 'Today';\n    } else if (days == 1) {\n      return 'Yesterday';\n    } else if (days > 1 && days <= 6) {\n      return '$days days ago';\n    } else if (days > 6 && days <= 30) {\n      final int weeks = days ~/ 7;\n      return weeks == 1 ? '$weeks week ago' : '$weeks weeks ago';\n    } else if (days > 30 && days <= 364) {\n      final int months = days ~/ 31;\n      return months == 1 ? '$months month ago' : '$months months ago';\n    } else {\n      final int years = days ~/ 365;\n      return years == 1 ? '$years year ago' : '$years years ago';\n    }\n  }\n\n  static String _todoStringElementAt(String value, int index) {\n    final List<String> todoStrSplitted = value.split(' ');\n    try {\n      return todoStrSplitted[index];\n    } on RangeError {\n      return '';\n    }\n  }\n\n  static bool _str2completion(String value) {\n    // A completed task starts with an lowercase x character.\n    return value == 'x';\n  }\n\n  static Priority _str2priority(String value) {\n    RegExpMatch? match = patternPriority.firstMatch(value);\n    if (match != null) {\n      String? priority = match.namedGroup('priority');\n      if (priority != null) {\n        return Priorities.byName(priority);\n      }\n    }\n\n    // Priority is optional.\n    return Priority.none;\n  }\n}\n"
  },
  {
    "path": "lib/todo/page/todo_create_edit_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart' show SnackBarHandler;\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/date_picker.dart';\nimport 'package:ntodotxt/common/widget/key_values_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\n\nclass TodoCreateEditPage extends StatelessWidget {\n  final Todo initTodo;\n  final Set<String> projects;\n  final Set<String> contexts;\n  final Set<String> keyValues;\n  final bool newTodo;\n\n  const TodoCreateEditPage({\n    required this.initTodo,\n    this.projects = const {},\n    this.contexts = const {},\n    this.keyValues = const {},\n    this.newTodo = true,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => TodoCubit(todo: initTodo),\n      child: GestureDetector(\n        onTap: () {\n          FocusScopeNode currentFocus = FocusScope.of(context);\n          if (!currentFocus.hasPrimaryFocus) {\n            currentFocus.unfocus();\n          }\n        },\n        child: TodoDialogWrapper(\n          initTodo: initTodo,\n          newTodo: newTodo,\n          child: Scaffold(\n            appBar: MainAppBar(\n              title: newTodo ? 'Create' : 'Edit',\n              toolbar: Row(\n                children: <Widget>[\n                  if (!newTodo) const DeleteTodoIconButton(),\n                  SaveTodoIconButton(initTodo: initTodo),\n                ],\n              ),\n            ),\n            body: ListView(\n              children: [\n                const TodoDescriptionTextField(),\n                const Divider(),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: ListTile(\n                    title: Text(\n                      'General',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                ),\n                const TodoPriorityItem(),\n                const Divider(),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: ListTile(\n                    title: Text(\n                      'Dates',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                ),\n                const TodoCreationDateItem(),\n                if (!newTodo) const TodoCompletionDateItem(),\n                const TodoDueDateItem(),\n                const Divider(),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: ListTile(\n                    title: Text(\n                      'Tags',\n                      style: Theme.of(context).textTheme.titleSmall,\n                    ),\n                  ),\n                ),\n                TodoProjectTagsItem(availableTags: projects),\n                TodoContextTagsItem(availableTags: contexts),\n                TodoKeyValueTagsItem(availableTags: keyValues),\n                const SizedBox(height: 16),\n              ],\n            ),\n            floatingActionButton: !newTodo ? DoneUndonePrimaryButton() : null,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass TodoDialogWrapper extends StatelessWidget {\n  final Widget child;\n  final Todo? initTodo;\n  final bool newTodo;\n\n  const TodoDialogWrapper({\n    required this.child,\n    required this.initTodo,\n    required this.newTodo,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      builder: (BuildContext context, TodoState state) {\n        return PopScope(\n          canPop: false,\n          onPopInvokedWithResult: <T>(bool didPop, T? result) async {\n            if (didPop) {\n              return;\n            }\n            if (state.todo.description.isEmpty) {\n              if (!await ConfirmationDialog.dialog(\n                context: context,\n                title: newTodo ? 'Create todo' : 'Edit todo',\n                message: 'Cannot save a todo with an empty name.',\n                cancelLabel: 'Cancel',\n                actionLabel: 'Continue',\n              )) {\n                if (context.mounted) {\n                  context.pop();\n                }\n              }\n            } else {\n              if (initTodo != state.todo) {\n                final bool confirm = await ConfirmationDialog.dialog(\n                  context: context,\n                  title: 'Save todo',\n                  message:\n                      'Todo contains unsaved changes. These will be irrecoverably lost.',\n                  actionLabel: 'Save',\n                  cancelLabel: 'Discard',\n                );\n                if (context.mounted && confirm) {\n                  context\n                      .read<TodoListBloc>()\n                      .add(TodoListTodoSubmitted(todo: state.todo));\n                  if (newTodo) {\n                    if (context.mounted) {\n                      SnackBarHandler.info(context, 'Todo has been created');\n                    }\n                  } else {\n                    if (context.mounted) {\n                      SnackBarHandler.info(context, 'Todo has been updated');\n                    }\n                  }\n                }\n              }\n              if (context.mounted) {\n                context.pop();\n              }\n            }\n          },\n          child: child,\n        );\n      },\n    );\n  }\n}\n\nclass DoneUndonePrimaryButton extends StatelessWidget {\n  const DoneUndonePrimaryButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      builder: (BuildContext context, TodoState state) {\n        return FloatingActionButton(\n          tooltip: state.todo.completion ? 'Done' : 'Undone',\n          child: state.todo.completion\n              ? Icon(Icons.remove_done)\n              : Icon(Icons.done_all),\n          onPressed: () async {\n            if (state.todo.completion) {\n              unsetCompletionDate(context);\n            } else {\n              setCompletionDate(context, state);\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void setCompletionDate(BuildContext context, TodoState state) {\n    context.read<TodoCubit>().toggleCompletion(\n          completion: true,\n          completionDate: DateTime.now(),\n        );\n  }\n\n  void unsetCompletionDate(BuildContext context) {\n    context.read<TodoCubit>().toggleCompletion(\n          completion: false,\n        );\n  }\n}\n\nclass SaveTodoIconButton extends StatelessWidget {\n  final Todo? initTodo;\n  const SaveTodoIconButton({\n    required this.initTodo,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      builder: (BuildContext context, TodoState state) {\n        return Visibility(\n          visible: initTodo != state.todo && state.todo.description.isNotEmpty,\n          child: IconButton(\n            tooltip: 'Save',\n            icon: const Icon(Icons.save),\n            onPressed: () async {\n              context\n                  .read<TodoListBloc>()\n                  .add(TodoListTodoSubmitted(todo: state.todo));\n              if (context.mounted) {\n                SnackBarHandler.info(context, 'Todo saved');\n                context.pop();\n              }\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DeleteTodoIconButton extends StatelessWidget {\n  const DeleteTodoIconButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      builder: (BuildContext context, TodoState state) {\n        return IconButton(\n          tooltip: 'Delete',\n          icon: const Icon(Icons.delete),\n          onPressed: () async {\n            final bool confirm = await ConfirmationDialog.dialog(\n              context: context,\n              title: 'Delete todo',\n              message: 'Do you want to delete the todo?',\n              actionLabel: 'Delete',\n              cancelLabel: 'Cancel',\n            );\n            if (context.mounted && confirm) {\n              context\n                  .read<TodoListBloc>()\n                  .add(TodoListTodoDeleted(todo: state.todo));\n              if (context.mounted) {\n                SnackBarHandler.info(context, 'Todo has been deleted');\n                context.pop();\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n}\n\nclass TodoDescriptionTextField extends StatefulWidget {\n  const TodoDescriptionTextField({super.key});\n\n  @override\n  State<TodoDescriptionTextField> createState() =>\n      _TodoDescriptionTextFieldState();\n}\n\nclass _TodoDescriptionTextFieldState extends State<TodoDescriptionTextField> {\n  late GlobalKey<FormFieldState> _textFormKey;\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _textFormKey = GlobalKey<FormFieldState>();\n    _controller = TextEditingController();\n  }\n\n  @override\n  void dispose() {\n    // Clean up the controller when the widget is disposed.\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      builder: (BuildContext context, TodoState state) {\n        // Setting text and selection together.\n        int base = _controller.selection.base.offset;\n        _controller.value = _controller.value.copyWith(\n          text: state.todo.description,\n          selection: TextSelection.fromPosition(\n            TextPosition(\n              offset: base < 0 || base > state.todo.description.length\n                  ? state.todo.description.length\n                  : base,\n            ),\n          ),\n        );\n        return TextFormField(\n          key: _textFormKey,\n          controller: _controller,\n          minLines: 1,\n          maxLines: 3,\n          keyboardType: TextInputType.text,\n          inputFormatters: [\n            FilteringTextInputFormatter.deny(RegExp(r'\\n')),\n          ],\n          style: Theme.of(context).textTheme.titleMedium,\n          textCapitalization: TextCapitalization.sentences,\n          decoration: const InputDecoration(\n            hintText: 'todo +project @context key:val',\n            contentPadding: EdgeInsets.symmetric(\n              horizontal: 20.0,\n              vertical: 16.0,\n            ),\n          ),\n          onChanged: (String value) =>\n              context.read<TodoCubit>().updateDescription(_controller.text),\n        );\n      },\n    );\n  }\n}\n\nclass TodoPriorityItem extends StatelessWidget {\n  const TodoPriorityItem({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.priority != state.todo.priority;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.outlined_flag),\n            title: const Text('Priority'),\n            subtitle: Text(state.todo.priority.name),\n            onTap: () => TodoPriorityTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<TodoCubit>(context),\n              availableTags: Priorities.priorities,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoProjectTagsItem extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const TodoProjectTagsItem({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.projects != state.todo.projects;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.rocket_launch_outlined),\n            title: const Text('Projects'),\n            subtitle: state.todo.projects.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.todo.projects) BasicChip(label: t),\n                    ],\n                  ),\n            onTap: () => TodoProjectTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<TodoCubit>(context),\n              availableTags: availableTags,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoContextTagsItem extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const TodoContextTagsItem({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.contexts != state.todo.contexts;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.tag),\n            title: const Text('Contexts'),\n            subtitle: state.todo.contexts.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.todo.contexts) BasicChip(label: t),\n                    ],\n                  ),\n            onTap: () => TodoContextTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<TodoCubit>(context),\n              availableTags: availableTags,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoKeyValueTagsItem extends StatelessWidget {\n  final Set<String> availableTags;\n\n  const TodoKeyValueTagsItem({\n    this.availableTags = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.keyValues != state.todo.keyValues;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.join_inner_outlined),\n            title: const Text('Key values'),\n            subtitle: state.todo.fmtKeyValues.isEmpty\n                ? const Text('-')\n                : GenericChipGroup(\n                    children: [\n                      for (var t in state.todo.fmtKeyValues)\n                        BasicChip(label: t),\n                    ],\n                  ),\n            onTap: () => TodoKeyValueTagDialog.dialog(\n              context: context,\n              cubit: BlocProvider.of<TodoCubit>(context),\n              availableTags: availableTags,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoCreationDateItem extends StatelessWidget {\n  const TodoCreationDateItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.creationDate != state.todo.creationDate;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.edit_calendar),\n            title: const Text('Creation date'),\n            subtitle: Text(\n              state.todo.creationDate != null\n                  ? state.todo.fmtCreationDate\n                  : Todo.date2Str(DateTime.now())!,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass TodoCompletionDateItem extends StatelessWidget {\n  const TodoCompletionDateItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.completionDate != state.todo.completionDate;\n      },\n      builder: (BuildContext context, TodoState state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.event_available),\n            title: const Text('Completion date'),\n            subtitle: Text(\n              state.todo.completionDate != null\n                  ? state.todo.fmtCompletionDate\n                  : '-',\n            ),\n            onTap: () async => await setCompletionDate(context, state),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> setCompletionDate(BuildContext context, TodoState state) async {\n    final DateTime? date = await TodoDatePicker.pickDate(\n      context: context,\n      initialDate: state.todo.completionDate,\n      endDateDaysOffset: 0, // You can not complete a todo in the future.\n    );\n    if (date != null) {\n      final String? formattedDate = Todo.date2Str(date);\n      if (formattedDate != null) {\n        if (context.mounted) {\n          context.read<TodoCubit>().toggleCompletion(\n                completion: true,\n                completionDate: date,\n              );\n        }\n      }\n    }\n  }\n}\n\nclass TodoDueDateItem extends StatelessWidget {\n  const TodoDueDateItem({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoCubit, TodoState>(\n      buildWhen: (TodoState previousState, TodoState state) {\n        return previousState.todo.keyValues != state.todo.keyValues;\n      },\n      builder: (BuildContext context, TodoState state) {\n        final String? dueDate = Todo.date2Str(state.todo.dueDate);\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: ListTile(\n            key: key,\n            leading: const Icon(Icons.event),\n            title: const Text('Due date'),\n            subtitle: Text(dueDate ?? '-'),\n            trailing: state.todo.dueDate == null\n                ? null\n                : IconButton(\n                    icon: const Icon(Icons.clear),\n                    onPressed: () => unsetDueDate(context, dueDate!),\n                  ),\n            onTap: () async => await setDueDate(context, state),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> setDueDate(BuildContext context, TodoState state) async {\n    final DateTime? date = await TodoDatePicker.pickDate(\n      context: context,\n      initialDate: state.todo.dueDate,\n      startDateDaysOffset: 0, // You can not set due date in the past.\n    );\n    if (date != null) {\n      final String? formattedDate = Todo.date2Str(date);\n      if (formattedDate != null) {\n        if (context.mounted) {\n          context.read<TodoCubit>().addKeyValue('due:$formattedDate');\n        }\n      }\n    }\n  }\n\n  void unsetDueDate(BuildContext context, String dueDate) {\n    context.read<TodoCubit>().removeKeyValue('due:$dueDate');\n  }\n}\n"
  },
  {
    "path": "lib/todo/page/todo_list_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/misc.dart' show PopScopeDrawer, SnackBarHandler;\nimport 'package:ntodotxt/common/widget/app_bar.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\nimport 'package:ntodotxt/common/widget/scroll_to_top.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/setting/state/interaction_settings_state.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo;\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\n\nclass TodoListPage extends StatelessWidget {\n  final Filter? filter;\n\n  const TodoListPage({\n    this.filter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) =>\n      filter == null ? _build(context) : _buildWithFilter(context);\n\n  Widget _build(BuildContext context) {\n    // @todo: Activate WideLayout later!\n    return const TodoListViewNarrow();\n    // final bool isNarrowLayout =\n    //     MediaQuery.of(context).size.width < maxScreenWidthCompact;\n    // return isNarrowLayout\n    //     ? const TodoListViewNarrow()\n    //     : const TodoListViewWide();\n  }\n\n  Widget _buildWithFilter(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => FilterCubit(\n        settingRepository: context.read<SettingRepository>(),\n        filterRepository: context.read<FilterRepository>(),\n        filter: filter,\n      )..load(),\n      child: Builder(\n        builder: (BuildContext context) => _build(context),\n      ),\n    );\n  }\n}\n\n///\n/// Narrow layout\n///\n\nclass TodoListViewNarrow extends ScollToTopView {\n  const TodoListViewNarrow({super.key});\n\n  @override\n  State<TodoListViewNarrow> createState() => _TodoListViewNarrowState();\n}\n\nclass _TodoListViewNarrowState extends ScollToTopViewState<TodoListViewNarrow> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoListBloc, TodoListState>(\n      buildWhen: (TodoListState previousState, TodoListState todoListState) =>\n          (previousState is! TodoListLoading &&\n              todoListState is TodoListLoading) ||\n          (previousState is TodoListLoading &&\n              todoListState is! TodoListLoading),\n      builder: (BuildContext context, TodoListState todoListState) {\n        return BlocBuilder<FilterCubit, FilterState>(\n          buildWhen: (FilterState previousState, FilterState filterState) =>\n              previousState.filter.name != filterState.filter.name,\n          builder: (BuildContext context, FilterState filterState) {\n            return PopScopeDrawer(\n              child: Scaffold(\n                appBar: MainAppBar(\n                  title: filterState.filter.name.isEmpty\n                      ? 'Todos'\n                      : 'Filter: ${filterState.filter.name}',\n                  toolbar: Row(\n                    children: [\n                      const TodoListDeleteFilter(),\n                      const TodoListSaveFilter(),\n                      IconButton(\n                        tooltip: 'Search',\n                        icon: const Icon(Icons.search),\n                        onPressed: () => context.pushNamed('todo-search',\n                            extra: filterState.filter),\n                      ),\n                    ],\n                  ),\n                  bottom: const AppBarFilterList(),\n                ),\n                drawer: Container(),\n                floatingActionButton: scrolledDown\n                    ? FloatingActionButton.small(\n                        tooltip: 'Go to top',\n                        foregroundColor:\n                            Theme.of(context).colorScheme.onSecondaryContainer,\n                        backgroundColor:\n                            Theme.of(context).colorScheme.secondaryContainer,\n                        child: const Icon(Icons.keyboard_arrow_up),\n                        onPressed: () => scrollToTop(),\n                      )\n                    : FloatingActionButton(\n                        tooltip: 'Add todo',\n                        child: const Icon(Icons.add),\n                        onPressed: () {\n                          String initDescription = '';\n                          if (filterState.filter.projects.isNotEmpty) {\n                            initDescription =\n                                '+${filterState.filter.projects.join(' +')}';\n                          }\n                          if (filterState.filter.contexts.isNotEmpty) {\n                            initDescription =\n                                '$initDescription @${filterState.filter.contexts.join(' @')}';\n                          }\n                          context.pushNamed(\n                            'todo-create',\n                            extra: Todo.fromString(value: initDescription),\n                          );\n                        },\n                      ),\n                body: RefreshIndicator(\n                  onRefresh: () async {\n                    context\n                        .read<TodoListBloc>()\n                        .add(const TodoListSynchronizationRequested());\n                  },\n                  child: LoadingIndicatorWrapper(\n                    loading: todoListState is TodoListLoading,\n                    child: TodoList(scrollController: scrollController),\n                  ),\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n///\n/// Wide layout\n///\n\nclass TodoListViewWide extends ScollToTopView {\n  const TodoListViewWide({super.key});\n\n  @override\n  State<TodoListViewWide> createState() => _TodoListViewWideState();\n}\n\nclass _TodoListViewWideState extends ScollToTopViewState<TodoListViewWide> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoListBloc, TodoListState>(\n      buildWhen: (TodoListState previousState, TodoListState todoListState) =>\n          (previousState is! TodoListLoading &&\n              todoListState is TodoListLoading) ||\n          (previousState is TodoListLoading &&\n              todoListState is! TodoListLoading),\n      builder: (BuildContext context, TodoListState todoListState) {\n        return BlocBuilder<FilterCubit, FilterState>(\n          buildWhen: (FilterState previousState, FilterState filterState) =>\n              previousState.filter.name != filterState.filter.name,\n          builder: (BuildContext context, FilterState filterState) {\n            return PopScopeDrawer(\n              child: Scaffold(\n                appBar: MainAppBar(\n                  title: filterState.filter.name.isEmpty\n                      ? 'Todos'\n                      : 'Filter: ${filterState.filter.name}',\n                  toolbar: Row(\n                    children: [\n                      const TodoListDeleteFilter(),\n                      const TodoListSaveFilter(),\n                      IconButton(\n                        tooltip: 'Search',\n                        icon: const Icon(Icons.search),\n                        onPressed: () => context.pushNamed('todo-search',\n                            extra: filterState.filter),\n                      ),\n                    ],\n                  ),\n                  bottom: const AppBarFilterList(),\n                ),\n                floatingActionButton: scrolledDown\n                    ? FloatingActionButton.small(\n                        tooltip: 'Go to top',\n                        foregroundColor:\n                            Theme.of(context).colorScheme.onSecondaryContainer,\n                        backgroundColor:\n                            Theme.of(context).colorScheme.secondaryContainer,\n                        child: const Icon(Icons.keyboard_arrow_up),\n                        onPressed: () => scrollToTop(),\n                      )\n                    : FloatingActionButton(\n                        tooltip: 'Add todo',\n                        child: const Icon(Icons.add),\n                        onPressed: () {\n                          String initDescription = '';\n                          if (filterState.filter.projects.isNotEmpty) {\n                            initDescription =\n                                '+${filterState.filter.projects.join(' +')}';\n                          }\n                          if (filterState.filter.contexts.isNotEmpty) {\n                            initDescription =\n                                '$initDescription @${filterState.filter.contexts.join(' @')}';\n                          }\n                          context.pushNamed(\n                            'todo-create',\n                            extra: Todo.fromString(value: initDescription),\n                          );\n                        },\n                      ),\n                body: RefreshIndicator(\n                  onRefresh: () async {\n                    context\n                        .read<TodoListBloc>()\n                        .add(const TodoListSynchronizationRequested());\n                  },\n                  child: LoadingIndicatorWrapper(\n                    loading: todoListState is TodoListLoading,\n                    child: TodoList(scrollController: scrollController),\n                  ),\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n///\n/// Components\n///\n\nclass LoadingIndicatorWrapper extends StatelessWidget {\n  final Widget child;\n  final bool loading;\n\n  const LoadingIndicatorWrapper({\n    required this.child,\n    this.loading = false,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    if (loading) {\n      return Stack(\n        children: <Widget>[\n          child,\n          // Custom progress indicator.\n          Align(\n            alignment: Alignment.topCenter,\n            child: Container(\n              padding: const EdgeInsets.only(top: 50),\n              child: Container(\n                height: 40,\n                width: 40,\n                decoration: BoxDecoration(\n                  color: Theme.of(context).colorScheme.primaryContainer,\n                  borderRadius: BorderRadius.circular(100),\n                ),\n                child: const Padding(\n                  padding: EdgeInsets.all(10),\n                  child: CircularProgressIndicator(strokeWidth: 3),\n                ),\n              ),\n            ),\n          ),\n        ],\n      );\n    } else {\n      return child;\n    }\n  }\n}\n\nclass TodoList extends StatelessWidget {\n  final ScrollController scrollController;\n\n  const TodoList({\n    required this.scrollController,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TodoListBloc, TodoListState>(\n      builder: (BuildContext context, TodoListState todoListState) {\n        return BlocBuilder<FilterCubit, FilterState>(\n          builder: (BuildContext context, FilterState filterState) {\n            final Map<String, Iterable<Todo>?> sectionList =\n                todoListState.groupedTodoList(\n              filterState.filter,\n            );\n            return ListView.builder(\n              key: const PageStorageKey('TodoList'),\n              controller: scrollController,\n              physics: const AlwaysScrollableScrollPhysics(),\n              itemCount: sectionList.length,\n              itemBuilder: (BuildContext context, int index) {\n                String section = sectionList.keys.elementAt(index);\n                Iterable<Todo> todoList = sectionList[section]!;\n                return Column(\n                  children: [\n                    Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                      child: ListTile(\n                        key: PageStorageKey<String>(section),\n                        title: Text(\n                          section,\n                          style: Theme.of(context).textTheme.titleSmall,\n                        ),\n                      ),\n                    ),\n                    for (var todo in todoList)\n                      Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                        child: TodoListTile(todo: todo),\n                      ),\n                    if (index < sectionList.length - 1) const Divider(),\n                  ],\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass TodoListTile extends StatelessWidget {\n  final Todo todo;\n\n  TodoListTile({\n    required this.todo,\n    Key? key,\n  }) : super(key: PageStorageKey<String>(todo.id));\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<InteractionSettingsCubit, InteractionSettingsState>(\n      buildWhen: (InteractionSettingsState previousState,\n              InteractionSettingsState state) =>\n          previousState.swipeLeftActionEnabled !=\n              state.swipeLeftActionEnabled ||\n          previousState.swipeRightActionEnabled !=\n              state.swipeRightActionEnabled,\n      builder: (BuildContext context, InteractionSettingsState state) {\n        return Dismissible(\n          key: Key(todo.id),\n          direction:\n              state.swipeLeftActionEnabled && state.swipeRightActionEnabled\n                  ? DismissDirection.horizontal\n                  : state.swipeLeftActionEnabled\n                      ? DismissDirection.endToStart\n                      : state.swipeRightActionEnabled\n                          ? DismissDirection.startToEnd\n                          : DismissDirection.none,\n          dismissThresholds: const {\n            DismissDirection.startToEnd: 0.5,\n            DismissDirection.endToStart: 0.5,\n          },\n          background: Container(\n            color: Theme.of(context).colorScheme.error, // red\n            alignment: Alignment.centerLeft,\n            padding: const EdgeInsets.only(left: 16.0),\n            child: Icon(Icons.delete),\n          ),\n          secondaryBackground: Container(\n            color: Theme.of(context).colorScheme.primaryContainer, // blue\n            alignment: Alignment.centerRight,\n            padding: const EdgeInsets.only(right: 16.0),\n            child: Icon(todo.completion ? Icons.remove_done : Icons.done_all),\n          ),\n          confirmDismiss: (DismissDirection direction) async {\n            // Delete\n            if (direction == DismissDirection.startToEnd) {\n              return await ConfirmationDialog.dialog(\n                context: context,\n                title: 'Delete todo',\n                message: 'Do you want to delete the todo?',\n                actionLabel: 'Delete',\n                cancelLabel: 'Cancel',\n              );\n            }\n            return true;\n          },\n          onDismissed: (DismissDirection direction) async {\n            // Done / Undone\n            if (todo.completion) {\n              SnackBarHandler.info(context, 'Todo has marked as not completed');\n            } else {\n              SnackBarHandler.info(context, 'Todo has marked as completed');\n            }\n            if (direction == DismissDirection.endToStart) {\n              context.read<TodoListBloc>().add(\n                    TodoListTodoCompletionToggled(\n                      todo: todo,\n                      completion: !todo.completion,\n                    ),\n                  );\n            } else if (direction == DismissDirection.startToEnd) {\n              // Delete\n              context.read<TodoListBloc>().add(\n                    TodoListTodoDeleted(\n                      todo: todo,\n                    ),\n                  );\n              SnackBarHandler.info(context, 'Todo has been deleted');\n            }\n          },\n          child: ListTile(\n            key: key,\n            title: _buildTitle(context),\n            subtitle: Padding(\n              padding: const EdgeInsets.only(top: 2.0),\n              child: _buildSubtitle(),\n            ),\n            onTap: () => context.pushNamed('todo-edit', extra: todo),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildTitle(BuildContext context) {\n    final List<String> items = todo.description.split(' ')\n      ..removeWhere(\n        (String item) => item.startsWith('due:'),\n      );\n\n    return RichText(\n      text: TextSpan(\n        style: todo.completion\n            ? Theme.of(context).textTheme.titleMedium?.copyWith(\n                  decoration: TextDecoration.lineThrough,\n                  decorationThickness: 4.0,\n                )\n            : Theme.of(context).textTheme.titleMedium,\n        text: '',\n        children: <TextSpan>[\n          for (int i = 0; i < items.length; i++)\n            TextSpan(\n                text: i == items.length - 1 ? items[i] : '${items[i]} ',\n                style: Todo.matchProject(items[i]) ||\n                        Todo.matchContext(items[i]) ||\n                        Todo.matchKeyValue(items[i])\n                    ? const TextStyle(fontWeight: FontWeight.bold)\n                    : null),\n        ],\n      ),\n      maxLines: 2,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  Widget? _buildSubtitle() {\n    return Wrap(\n      crossAxisAlignment: WrapCrossAlignment.center,\n      spacing: 2.0, // gap between adjacent chips\n      runSpacing: 2.0, // gap between lines\n      children: <Widget>[\n        if (todo.priority != Priority.none)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.flag_outlined,\n            label: todo.priority.name,\n          ),\n        if (todo.creationDate != null)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.edit_calendar,\n            label: Todo.differenceToToday(todo.creationDate!),\n          ),\n        if (todo.completionDate != null && todo.completion)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.event_available,\n            label: Todo.differenceToToday(todo.completionDate!),\n          ),\n        if (todo.dueDate != null)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.event,\n            label: Todo.date2Str(todo.dueDate!)!,\n          )\n      ],\n    );\n  }\n}\n\nclass TodoListDeleteFilter extends StatelessWidget {\n  const TodoListDeleteFilter({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        return state.changed || state.filter.id == null\n            ? Container()\n            : IconButton(\n                tooltip: 'Delete filter',\n                icon: const Icon(Icons.delete),\n                onPressed: () async {\n                  final bool confirm = await ConfirmationDialog.dialog(\n                    context: context,\n                    title: 'Delete filter',\n                    message: 'Do you want to delete the filter?',\n                    actionLabel: 'Delete',\n                    cancelLabel: 'Cancel',\n                  );\n                  if (context.mounted && confirm) {\n                    await context.read<FilterCubit>().delete(state.filter);\n                    if (context.mounted) {\n                      SnackBarHandler.info(context, 'Filter deleted');\n                      context.pop();\n                      context.read<DrawerCubit>().back();\n                    }\n                  }\n                },\n              );\n      },\n    );\n  }\n}\n\nclass TodoListSaveFilter extends StatelessWidget {\n  const TodoListSaveFilter({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterCubit, FilterState>(\n      builder: (BuildContext context, FilterState state) {\n        return !state.changed || state.filter.id == null\n            ? Container()\n            : IconButton(\n                tooltip: 'Save filter',\n                icon: const Icon(Icons.save),\n                onPressed: () async {\n                  await context\n                      .read<FilterCubit>()\n                      .update(state.filter.copyWith());\n                  if (context.mounted) {\n                    SnackBarHandler.info(context, 'Filter saved');\n                  }\n                },\n              );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/todo/page/todo_search_page.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\n\nclass TodoSearchPage extends StatelessWidget {\n  final Filter? filter;\n\n  const TodoSearchPage({\n    this.filter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => FilterCubit(\n        settingRepository: context.read<SettingRepository>(),\n        filterRepository: context.read<FilterRepository>(),\n        filter: filter,\n      )..load(),\n      child: Builder(\n        builder: (BuildContext context) => const TodoSearchView(),\n      ),\n    );\n  }\n}\n\nclass TodoSearchView extends StatefulWidget {\n  const TodoSearchView({super.key});\n\n  @override\n  State<TodoSearchView> createState() => _TodoSearchViewState();\n}\n\nclass _TodoSearchViewState extends State<TodoSearchView> {\n  String query = '';\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = TextEditingController();\n    _controller.text = query;\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  Iterable<Todo> _getResults(Iterable<Todo> todoList) {\n    if (query.trim().isEmpty) {\n      return Iterable.empty();\n    } else {\n      return todoList.where(\n        (Todo t) => t.toString().toLowerCase().contains(query.toLowerCase()),\n      );\n    }\n  }\n\n  Widget _buildSearchField(BuildContext context) {\n    return TextFormField(\n      controller: _controller,\n      keyboardType: TextInputType.text,\n      inputFormatters: [\n        FilteringTextInputFormatter.deny(RegExp(r'\\n')),\n      ],\n      style: Theme.of(context).textTheme.titleLarge,\n      textCapitalization: TextCapitalization.sentences,\n      decoration: const InputDecoration(hintText: 'Search ...'),\n      onChanged: (String value) => setState(() => query = _controller.text),\n    );\n  }\n\n  List<Widget> _buildActions(BuildContext context) {\n    return [\n      IconButton(\n        icon: const Icon(Icons.clear),\n        onPressed: query.isEmpty\n            ? null\n            : () {\n                setState(() => query = '');\n                _controller.text = query;\n              },\n      ),\n      const SizedBox(width: 8),\n    ];\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // @todo: Activate WideLayout later!\n    // final bool narrowView =\n    //     MediaQuery.of(context).size.width < maxScreenWidthCompact;\n\n    return GestureDetector(\n      onTap: () {\n        FocusScopeNode currentFocus = FocusScope.of(context);\n        if (!currentFocus.hasPrimaryFocus) {\n          currentFocus.unfocus();\n        }\n      },\n      child: Scaffold(\n        appBar: AppBar(\n          // titleSpacing: narrowView ? 0.0 : null,\n          titleSpacing: 0.0,\n          title: _buildSearchField(context),\n          actions: _buildActions(context),\n        ),\n        body: BlocBuilder<TodoListBloc, TodoListState>(\n          builder: (BuildContext context, TodoListState todoListState) {\n            return BlocBuilder<FilterCubit, FilterState>(\n              builder: (BuildContext context, FilterState filterState) {\n                final List<Todo> matchQuery = _getResults(\n                  todoListState.filteredTodoList(filterState.filter),\n                ).toList();\n                return ListView.builder(\n                  padding: const EdgeInsets.symmetric(vertical: 8.0),\n                  itemCount: matchQuery.length,\n                  itemBuilder: (BuildContext context, int index) {\n                    Todo todo = matchQuery[index];\n                    return TodoSearchTile(todo: todo);\n                  },\n                );\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass TodoSearchTile extends StatelessWidget {\n  final Todo todo;\n\n  TodoSearchTile({\n    required this.todo,\n    Key? key,\n  }) : super(key: PageStorageKey<String>(todo.id));\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      child: ListTile(\n        key: key,\n        title: _buildTitle(context),\n        subtitle: Padding(\n          padding: const EdgeInsets.only(top: 2.0),\n          child: _buildSubtitle(),\n        ),\n        onTap: () => _onTapAction(context),\n      ),\n    );\n  }\n\n  Widget _buildTitle(BuildContext context) {\n    final List<String> items = todo.description.split(' ')\n      ..removeWhere(\n        (String item) => item.startsWith('due:'),\n      );\n\n    return RichText(\n      text: TextSpan(\n        style: todo.completion\n            ? Theme.of(context).textTheme.titleMedium?.copyWith(\n                  decoration: TextDecoration.lineThrough,\n                  decorationThickness: 4.0,\n                )\n            : Theme.of(context).textTheme.titleMedium,\n        text: '',\n        children: <TextSpan>[\n          for (int i = 0; i < items.length; i++)\n            TextSpan(\n                text: i == items.length - 1 ? items[i] : '${items[i]} ',\n                style: Todo.matchProject(items[i]) ||\n                        Todo.matchContext(items[i]) ||\n                        Todo.matchKeyValue(items[i])\n                    ? const TextStyle(fontWeight: FontWeight.bold)\n                    : null),\n        ],\n      ),\n      maxLines: 2,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  Widget? _buildSubtitle() {\n    return Wrap(\n      crossAxisAlignment: WrapCrossAlignment.center,\n      spacing: 2.0, // gap between adjacent chips\n      runSpacing: 2.0, // gap between lines\n      children: <Widget>[\n        if (todo.priority != Priority.none)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.flag_outlined,\n            label: todo.priority.name,\n          ),\n        if (todo.creationDate != null)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.edit_calendar,\n            label: Todo.differenceToToday(todo.creationDate!),\n          ),\n        if (todo.completionDate != null && todo.completion)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.event_available,\n            label: Todo.differenceToToday(todo.completionDate!),\n          ),\n        if (todo.dueDate != null)\n          BasicIconChip(\n            mono: true,\n            iconData: Icons.event,\n            label: Todo.date2Str(todo.dueDate!)!,\n          )\n      ],\n    );\n  }\n\n  void _onTapAction(BuildContext context) {\n    context.pushNamed('todo-edit', extra: todo);\n  }\n}\n"
  },
  {
    "path": "lib/todo/repository/todo_list_repository.dart",
    "content": "import 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\n\n/// A repository that handles `todo` related requests.\nclass TodoListRepository {\n  final TodoListApi api;\n\n  TodoListRepository(this.api);\n\n  /// Provides a [Stream] of all todos read from the source.\n  Stream<List<Todo>> getTodoList() => api.getTodoList();\n\n  Future<void> initSource() => api.initSource();\n\n  /// Read [todoList] from source.\n  Future<void> readFromSource() => api.readFromSource();\n\n  /// Write [todoList] to source.\n  Future<void> writeToSource() => api.writeToSource();\n\n  bool existsTodo(Todo todo) => api.existsTodo(todo);\n\n  /// Saves a [todo].\n  /// If a [todo] with [id] already exists, it will be replaced.\n  /// If the [todo] with [id] already exists it will be updated/merged.\n  void saveTodo(Todo todo) => api.saveTodo(todo);\n\n  /// Saves multiple [todos] by [id] at once.\n  void saveMultipleTodos(List<Todo> todos) => api.saveMultipleTodos(todos);\n\n  /// Deletes the given [todo] by [id].\n  void deleteTodo(Todo todo) => api.deleteTodo(todo);\n\n  /// Deletes multiple [todos] by [id] at once.\n  void deleteMultipleTodos(List<Todo> todos) => api.deleteMultipleTodos(todos);\n}\n"
  },
  {
    "path": "lib/todo/state/todo_cubit.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/exception/exceptions.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\n\nclass TodoCubit extends Cubit<TodoState> {\n  TodoCubit({\n    required Todo todo,\n  }) : super(TodoSuccess(todo: todo));\n\n  void updateTodo(Todo todo) {\n    try {\n      emit(state.success(\n        todo: todo.copyWith(),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void toggleCompletion({bool? completion, DateTime? completionDate}) {\n    try {\n      emit(state.success(\n        todo: state.todo.copyWith(\n          completion: completionDate != null ? true : !state.todo.completion,\n          completionDate: completionDate,\n        ),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateDescription(String description) {\n    try {\n      emit(state.success(\n        todo: state.todo.copyWith(description: description),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void setPriority(Priority priority) {\n    try {\n      emit(state.success(\n        todo: state.todo.copyWith(priority: priority),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void unsetPriority() {\n    try {\n      emit(state.success(\n        todo: state.todo.copyWith(priority: Priority.none),\n      ));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addProject(String project) {\n    try {\n      if (state.todo.containsProject(project)) {\n        emit(\n          state.success(todo: state.todo.copyWith()),\n        );\n      } else {\n        if (!Todo.matchProject(Todo.fmtProject(project))) {\n          throw TodoInvalidProjectTag(tag: project);\n        }\n        emit(\n          state.success(\n            todo: state.todo.copyWith(\n              description:\n                  '${state.todo.description} ${Todo.fmtProject(project)}',\n            ),\n          ),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateProjects(Set<String> projects) {\n    try {\n      for (String project in projects) {\n        if (!Todo.matchProject(Todo.fmtProject(project))) {\n          throw TodoInvalidProjectTag(tag: project);\n        }\n      }\n      String description = state.todo.description;\n      Iterable<String> addProjects =\n          projects.where((p) => !state.todo.containsProject(p));\n      Iterable<String> removeProjects =\n          state.todo.projects.where((p) => !projects.contains(p));\n      // Remove projects\n      for (String p in removeProjects) {\n        description = description.replaceAll(Todo.fmtProject(p), '');\n      }\n      // Add projects\n      description = '$description ${{\n        for (String p in addProjects) Todo.fmtProject(p)\n      }.join(\" \")}';\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: description,\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removeProject(String project) {\n    try {\n      if (!Todo.matchProject(Todo.fmtProject(project))) {\n        throw TodoInvalidProjectTag(tag: project);\n      }\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: state.todo.description\n                .replaceAll(Todo.fmtProject(project), '')\n                .trim(),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addContext(String context) {\n    try {\n      if (state.todo.containsContext(context)) {\n        emit(\n          state.success(todo: state.todo.copyWith()),\n        );\n      } else {\n        if (!Todo.matchContext(Todo.fmtContext(context))) {\n          throw TodoInvalidContextTag(tag: context);\n        }\n        emit(\n          state.success(\n            todo: state.todo.copyWith(\n              description:\n                  '${state.todo.description} ${Todo.fmtContext(context)}',\n            ),\n          ),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateContexts(Set<String> contexts) {\n    try {\n      for (String context in contexts) {\n        if (!Todo.matchContext(Todo.fmtContext(context))) {\n          throw TodoInvalidContextTag(tag: context);\n        }\n      }\n      String description = state.todo.description;\n      Iterable<String> addContexts =\n          contexts.where((c) => !state.todo.containsContext(c));\n      Iterable<String> removeContexts =\n          state.todo.contexts.where((c) => !contexts.contains(c));\n      // Remove projects\n      for (String c in removeContexts) {\n        description = description.replaceAll(Todo.fmtContext(c), '');\n      }\n      // Add projects\n      description = '$description ${{\n        for (String c in addContexts) Todo.fmtContext(c)\n      }.join(\" \")}';\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: description,\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removeContext(String context) {\n    try {\n      if (!Todo.matchContext(Todo.fmtContext(context))) {\n        throw TodoInvalidContextTag(tag: context);\n      }\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: state.todo.description\n                .replaceAll(Todo.fmtContext(context), '')\n                .trim(),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void addKeyValue(String kv) {\n    try {\n      if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) {\n        throw TodoInvalidKeyValueTag(tag: kv);\n      }\n      if (state.todo.containsKeyValue(kv)) {\n        emit(\n          state.success(\n            todo: state.todo.copyWith(\n              description: state.todo.description.replaceAllMapped(\n                RegExp('${kv.split(':')[0]}:\\\\S+'),\n                (match) => Todo.fmtKeyValue(kv),\n              ),\n            ),\n          ),\n        );\n      } else {\n        emit(\n          state.success(\n            todo: state.todo.copyWith(\n              description: '${state.todo.description} ${Todo.fmtKeyValue(kv)}',\n            ),\n          ),\n        );\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void updateKeyValues(Set<String> keyValues) {\n    try {\n      for (String kv in keyValues) {\n        if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) {\n          throw TodoInvalidKeyValueTag(tag: kv);\n        }\n      }\n      String description = state.todo.description;\n      Iterable<String> addKeyValues =\n          keyValues.where((kv) => !state.todo.containsKeyValue(kv));\n      Iterable<String> removeKeyValues = state.todo.keyValues.where((kv) {\n        for (String keyVal in keyValues) {\n          if (kv.split(':')[0] == keyVal.split(':')[0]) {\n            return false;\n          }\n        }\n        return true;\n      });\n      Iterable<String> existingKeyValues =\n          keyValues.where((kv) => state.todo.containsKeyValue(kv));\n      // Remove projects\n      for (String kv in removeKeyValues) {\n        description = description.replaceAll(Todo.fmtKeyValue(kv), '');\n      }\n      description = '$description ${{\n        for (String kv in addKeyValues) Todo.fmtKeyValue(kv)\n      }.join(\" \")}';\n      // Replace existing key values instead concat them.\n      for (String kv in existingKeyValues) {\n        description = description.replaceAllMapped(\n          RegExp('${kv.split(':')[0]}:\\\\S+'),\n          (match) => Todo.fmtKeyValue(kv),\n        );\n      }\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: description,\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void removeKeyValue(String kv) {\n    try {\n      if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) {\n        throw TodoInvalidKeyValueTag(tag: kv);\n      }\n      emit(\n        state.success(\n          todo: state.todo.copyWith(\n            description: state.todo.description\n                .replaceAll(Todo.fmtKeyValue(kv), '')\n                .trim(),\n          ),\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/todo/state/todo_list_bloc.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/repository/todo_list_repository.dart'\n    show TodoListRepository;\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\n\nclass TodoListBloc extends Bloc<TodoListEvent, TodoListState> {\n  final TodoListRepository _repository;\n\n  TodoListBloc({\n    required TodoListRepository repository,\n  })  : _repository = repository,\n        super(const TodoListLoading()) {\n    on<TodoListSubscriptionRequested>(_onTodoListSubscriptionRequested);\n    on<TodoListSynchronizationRequested>(_onTodoListSynchronizationRequested);\n    on<TodoListTodoSubmitted>(_onTodoSubmitted);\n    on<TodoListTodoDeleted>(_onTodoDeleted);\n    on<TodoListTodoCompletionToggled>(_onTodoCompletionToggled);\n  }\n\n  Future<void> _onTodoListSubscriptionRequested(\n    TodoListSubscriptionRequested event,\n    Emitter<TodoListState> emit,\n  ) async {\n    await emit.forEach<List<Todo>>(\n      _repository.getTodoList(),\n      onData: (todoList) {\n        // Use copyWith here to keep the state (e.g. if loading)\n        return state.copyWith(todoList: todoList);\n      },\n      onError: (e, _) => state.error(message: e.toString()),\n    );\n  }\n\n  void _onTodoListSynchronizationRequested(\n    TodoListSynchronizationRequested event,\n    Emitter<TodoListState> emit,\n  ) async {\n    try {\n      // Initialize only if this is the first time.\n      if (state is TodoListLoading) {\n        await _repository\n            .initSource()\n            .whenComplete(() => emit(state.success()));\n      } else {\n        emit(state.loading());\n        await _repository\n            .readFromSource()\n            .whenComplete(() => emit(state.success()));\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void _onTodoSubmitted(\n    TodoListTodoSubmitted event,\n    Emitter<TodoListState> emit,\n  ) async {\n    emit(state.loading());\n    try {\n      _repository.saveTodo(event.todo.copyWith());\n      await _repository\n          .writeToSource()\n          .whenComplete(() => emit(state.success()));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void _onTodoDeleted(\n    TodoListTodoDeleted event,\n    Emitter<TodoListState> emit,\n  ) async {\n    emit(state.loading());\n    try {\n      _repository.deleteTodo(event.todo.copyWith());\n      await _repository\n          .writeToSource()\n          .whenComplete(() => emit(state.success()));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  void _onTodoCompletionToggled(\n    TodoListTodoCompletionToggled event,\n    Emitter<TodoListState> emit,\n  ) async {\n    emit(state.loading());\n    try {\n      if (_repository.existsTodo(event.todo)) {\n        _repository.saveTodo(\n          event.todo.copyDiff(completion: event.completion),\n        );\n      } else {\n        _repository.saveTodo(\n          event.todo.copyWith(completion: event.completion),\n        );\n      }\n      await _repository\n          .writeToSource()\n          .whenComplete(() => emit(state.success()));\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/todo/state/todo_list_event.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\n\nsealed class TodoListEvent extends Equatable {\n  const TodoListEvent();\n\n  @override\n  List<Object?> get props => [];\n}\n\nfinal class TodoListSubscriptionRequested extends TodoListEvent {\n  const TodoListSubscriptionRequested();\n}\n\nfinal class TodoListSynchronizationRequested extends TodoListEvent {\n  const TodoListSynchronizationRequested();\n}\n\nfinal class TodoListTodoSubmitted extends TodoListEvent {\n  final Todo todo;\n\n  const TodoListTodoSubmitted({\n    required this.todo,\n  });\n\n  @override\n  List<Object?> get props => [todo];\n}\n\nfinal class TodoListTodoDeleted extends TodoListEvent {\n  final Todo todo;\n\n  const TodoListTodoDeleted({\n    required this.todo,\n  });\n\n  @override\n  List<Object?> get props => [todo];\n}\n\nfinal class TodoListTodoCompletionToggled extends TodoListEvent {\n  final Todo todo;\n  final bool completion;\n\n  const TodoListTodoCompletionToggled({\n    required this.todo,\n    required this.completion,\n  });\n\n  @override\n  List<Object?> get props => [todo, completion];\n}\n"
  },
  {
    "path": "lib/todo/state/todo_list_state.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\n\nsealed class TodoListState extends Equatable {\n  final List<Todo> todoList;\n\n  const TodoListState({\n    this.todoList = const [],\n  });\n\n  /// Returns a list with all projects of all todos.\n  Set<String> get projects {\n    // @todo: Sort\n    return todoList.map((Todo todo) => todo.projects).fold<Set<String>>(\n      {},\n      (Set<String> previousValue, Set<String> value) =>\n          previousValue..addAll(value),\n    );\n  }\n\n  /// Returns a list with all contexts of all todos.\n  Set<String> get contexts {\n    // @todo: Sort\n    return todoList.map((Todo todo) => todo.contexts).fold<Set<String>>(\n      {},\n      (Set<String> previousValue, Set<String> value) =>\n          previousValue..addAll(value),\n    );\n  }\n\n  /// Returns a list with all key values of all todos.\n  Set<String> get keyValues {\n    // @todo: Sort\n    return todoList.map((Todo todo) => todo.fmtKeyValues).fold<Set<String>>(\n      {},\n      (Set<String> previousValue, Set<String> value) =>\n          previousValue..addAll(value),\n    );\n  }\n\n  Iterable<Todo> filteredTodoList(Filter filter) => filter.apply(todoList);\n\n  Map<String, Iterable<Todo>> groupedTodoList(Filter filter) =>\n      filter.grouped(filteredTodoList(filter));\n\n  TodoListState copyWith({\n    List<Todo>? todoList,\n  });\n\n  TodoListState loading({\n    List<Todo>? todoList,\n  }) {\n    return TodoListLoading(\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  TodoListState success({\n    List<Todo>? todoList,\n  }) {\n    return TodoListSuccess(\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  TodoListState error({\n    required String message,\n    List<Todo>? todoList,\n  }) {\n    return TodoListError(\n      message: message,\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        todoList,\n      ];\n\n  @override\n  String toString() => 'TodoListState { }';\n}\n\nfinal class TodoListLoading extends TodoListState {\n  const TodoListLoading({\n    super.todoList,\n  });\n\n  @override\n  TodoListLoading copyWith({\n    List<Todo>? todoList,\n  }) {\n    return TodoListLoading(\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  @override\n  String toString() =>\n      'TodoListLoading { todos: ${[for (var t in todoList) t]} }';\n}\n\nfinal class TodoListSuccess extends TodoListState {\n  const TodoListSuccess({\n    super.todoList,\n  });\n\n  @override\n  TodoListSuccess copyWith({\n    List<Todo>? todoList,\n  }) {\n    return TodoListSuccess(\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  @override\n  String toString() =>\n      'TodoListSuccess { todos: ${[for (var t in todoList) t]} }';\n}\n\nfinal class TodoListError extends TodoListState {\n  final String message;\n\n  const TodoListError({\n    required this.message,\n    super.todoList,\n  });\n\n  @override\n  TodoListError copyWith({\n    String? message,\n    List<Todo>? todoList,\n  }) {\n    return TodoListError(\n      message: message ?? this.message,\n      todoList: todoList ?? this.todoList,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        message,\n        todoList,\n      ];\n\n  @override\n  String toString() => 'TodoListError { message: $message }';\n}\n"
  },
  {
    "path": "lib/todo/state/todo_state.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\n\nsealed class TodoState extends Equatable {\n  final Todo todo;\n\n  const TodoState({\n    required this.todo,\n  });\n\n  TodoSuccess success({\n    Todo? todo,\n  }) {\n    return TodoSuccess(\n      todo: todo ?? this.todo,\n    );\n  }\n\n  TodoError error({\n    required String message,\n    Todo? todo,\n  }) {\n    return TodoError(\n      message: message,\n      todo: todo ?? this.todo,\n    );\n  }\n\n  @override\n  List<Object> get props => [\n        todo,\n      ];\n\n  @override\n  String toString() => 'TodoState { todo: $todo }';\n}\n\nfinal class TodoSuccess extends TodoState {\n  const TodoSuccess({\n    required super.todo,\n  });\n\n  TodoSuccess copyWith({\n    Todo? todo,\n  }) {\n    return TodoSuccess(\n      todo: todo ?? this.todo,\n    );\n  }\n\n  @override\n  List<Object> get props => [\n        todo,\n      ];\n\n  @override\n  String toString() => 'TodoSuccess { todo: \"$todo\" }';\n}\n\nfinal class TodoError extends TodoState {\n  final String message;\n\n  const TodoError({\n    required this.message,\n    required super.todo,\n  });\n\n  TodoError copyWith({\n    String? message,\n    Todo? todo,\n  }) {\n    return TodoError(\n      message: message ?? this.message,\n      todo: todo ?? this.todo,\n    );\n  }\n\n  @override\n  List<Object> get props => [\n        message,\n        todo,\n      ];\n\n  @override\n  String toString() => 'TodoError { message: $message todo: \"$todo\" }';\n}\n"
  },
  {
    "path": "lib/todo_file/state/todo_file_cubit.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:ntodotxt/common/constants/app.dart';\nimport 'package:ntodotxt/main.dart' show log;\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\n\nclass TodoFileCubit extends Cubit<TodoFileState> {\n  final SettingRepository repository;\n\n  TodoFileCubit({\n    required this.repository,\n    String todoFilename = defaultTodoFilename,\n    String doneFilename = defaultDoneFilename,\n    String localPath = defaultLocalTodoPath,\n    String remotePath = defaultRemoteTodoPath,\n    TodoFileState? state,\n  }) : super(\n          state ??\n              TodoFileLoading(\n                todoFilename: todoFilename,\n                doneFilename: doneFilename,\n                localPath: localPath,\n                remotePath: remotePath,\n              ),\n        );\n\n  Future<void> checkLocalPermission(String filename) async {\n    try {\n      await File(filename).create();\n    } catch (e) {\n      rethrow;\n    }\n  }\n\n  Future<void> load() async {\n    try {\n      log.info('Retrieving setting \\'todoFilename\\'.');\n      Setting? todoFilename = await repository.get(key: 'todoFilename');\n      if (todoFilename != null) {\n        emit(state.load(todoFilename: todoFilename.value));\n      }\n\n      log.info('Retrieving setting \\'localPath\\'.');\n      final Setting? localPath = await repository.get(key: 'localPath');\n      if (localPath != null) {\n        emit(state.load(localPath: localPath.value));\n      }\n      await checkLocalPermission(state.localTodoFilePath);\n\n      log.info('Retrieving setting \\'remotePath\\'.');\n      final Setting? remotePath = await repository.get(key: 'remotePath');\n      if (remotePath != null) {\n        emit(state.load(remotePath: remotePath.value));\n      }\n\n      emit(state.ready());\n    } on Exception catch (e) {\n      emit(\n        state.error(\n          message: e.toString(),\n          todoFilename: defaultTodoFilename,\n          doneFilename: defaultDoneFilename,\n          localPath: defaultLocalTodoPath,\n          remotePath: defaultRemoteTodoPath,\n        ),\n      );\n    }\n  }\n\n  Future<void> saveLocalPath(String? value) async {\n    try {\n      if (value != null) {\n        emit(state.load());\n        log.fine('Saving setting \\'localPath\\'.');\n        await repository.updateOrInsert(\n          Setting(key: 'localPath', value: value),\n        );\n        emit(state.ready(localPath: value));\n      }\n    } on Exception catch (e) {\n      emit(\n        state.error(\n          message: e.toString(),\n          localPath: value,\n        ),\n      );\n    }\n  }\n\n  Future<void> saveLocalFilename(String? value) async {\n    try {\n      if (value != null) {\n        emit(state.load());\n        log.fine('Saving setting \\'todoFilename\\'.');\n        await repository.updateOrInsert(\n          Setting(key: 'todoFilename', value: value),\n        );\n        emit(state.ready(todoFilename: value));\n      }\n    } on Exception catch (e) {\n      emit(\n        state.error(\n          message: e.toString(),\n          todoFilename: value,\n        ),\n      );\n    }\n  }\n\n  Future<void> saveRemotePath(String? value) async {\n    try {\n      if (value != null) {\n        emit(state.load());\n        log.fine('Saving setting \\'remotePath\\'.');\n        await repository.updateOrInsert(\n          Setting(key: 'remotePath', value: value),\n        );\n        emit(state.ready(remotePath: value));\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> resetToDefaults() async {\n    try {\n      emit(state.load());\n      log.fine('Resetting to the defaults.');\n      await resetTodoFileSettings();\n      emit(\n        TodoFileLoading(\n          todoFilename: defaultTodoFilename,\n          doneFilename: defaultDoneFilename,\n          localPath: defaultLocalTodoPath,\n          remotePath: defaultRemoteTodoPath,\n        ),\n      );\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n\n  Future<void> resetTodoFileSettings() async {\n    try {\n      log.fine('Resetting todofile settings.');\n      for (var k in [\n        'todoFilename',\n        'localPath',\n        'remotePath',\n      ]) {\n        log.fine('Deleting setting \\'$k\\'.');\n        await repository.delete(key: k);\n      }\n    } on Exception catch (e) {\n      emit(state.error(message: e.toString()));\n    }\n  }\n}\n"
  },
  {
    "path": "lib/todo_file/state/todo_file_state.dart",
    "content": "import 'dart:io';\n\nimport 'package:equatable/equatable.dart';\nimport 'package:ntodotxt/common/constants/app.dart';\n\nsealed class TodoFileState extends Equatable {\n  final String todoFilename;\n  final String doneFilename;\n  final String localPath;\n  final String remotePath;\n\n  TodoFileState({\n    this.todoFilename = defaultTodoFilename,\n    this.doneFilename = defaultDoneFilename,\n    String localPath = defaultLocalTodoPath,\n    String remotePath = defaultLocalTodoPath,\n  })  : localPath = localPath.endsWith(Platform.pathSeparator)\n            ? localPath\n            : '$localPath${Platform.pathSeparator}',\n        remotePath = remotePath.endsWith(Platform.pathSeparator)\n            ? remotePath\n            : '$remotePath${Platform.pathSeparator}';\n\n  String get localTodoFilePath => '$localPath$todoFilename';\n\n  String get remoteTodoFilePath => '$remotePath$todoFilename';\n\n  TodoFileLoading load({\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileLoading(\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  TodoFileReady ready({\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileReady(\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  TodoFileError error({\n    required String message,\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileError(\n      message: message,\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        todoFilename,\n        doneFilename,\n        localPath,\n        remotePath,\n      ];\n\n  @override\n  String toString() =>\n      'TodoFileState { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }';\n}\n\nfinal class TodoFileLoading extends TodoFileState {\n  TodoFileLoading({\n    super.todoFilename,\n    super.doneFilename,\n    super.localPath,\n    super.remotePath,\n  });\n\n  TodoFileLoading copyWith({\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileLoading(\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  @override\n  String toString() =>\n      'TodoFileLoading { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }';\n}\n\nfinal class TodoFileReady extends TodoFileState {\n  TodoFileReady({\n    super.todoFilename,\n    super.doneFilename,\n    super.localPath,\n    super.remotePath,\n  });\n\n  TodoFileReady copyWith({\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileReady(\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  @override\n  String toString() =>\n      'TodoFileReady { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }';\n}\n\nfinal class TodoFileError extends TodoFileState {\n  final String message;\n\n  TodoFileError({\n    required this.message,\n    super.todoFilename,\n    super.doneFilename,\n    super.localPath,\n    super.remotePath,\n  });\n\n  TodoFileError copyWith({\n    String? message,\n    String? todoFilename,\n    String? doneFilename,\n    String? localPath,\n    String? remotePath,\n  }) {\n    return TodoFileError(\n      message: message ?? this.message,\n      todoFilename: todoFilename ?? this.todoFilename,\n      doneFilename: doneFilename ?? this.doneFilename,\n      localPath: localPath ?? this.localPath,\n      remotePath: remotePath ?? this.remotePath,\n    );\n  }\n\n  @override\n  List<Object?> get props => [\n        message,\n        todoFilename,\n        doneFilename,\n        localPath,\n        remotePath,\n      ];\n\n  @override\n  String toString() =>\n      'TodoFileError { message $message localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }';\n}\n"
  },
  {
    "path": "lib/webdav/client/webdav_client.dart",
    "content": "import 'dart:convert' show utf8;\nimport 'dart:io';\n\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:ntodotxt/main.dart' show log;\nimport 'package:path/path.dart' as p;\nimport 'package:webdav_client/webdav_client.dart' as webdav;\n\nclass WebDAVClientException implements Exception {\n  final String message;\n\n  const WebDAVClientException(this.message);\n\n  @override\n  String toString() => message;\n}\n\nclass WebDAVClient {\n  webdav.Client? _client; // Singleton pattern\n\n  late final String scheme;\n  late final String host;\n  late final int? port;\n  late final String path;\n  final String username;\n  final String password;\n  final bool acceptUntrustedCert;\n\n  static const int idleTimeout = 15000;\n  static const int connectionTimeout = 15000;\n  static const int sendTimeout = 15000;\n  static const int receiveTimeout = 15000;\n\n  WebDAVClient({\n    required String server,\n    required String path,\n    required this.username,\n    required this.password,\n    this.acceptUntrustedCert = false,\n  }) {\n    final RegExp exp = RegExp(\n        r'(?<scheme>^(http|https)):\\/\\/(?<host>[a-zA-Z0-9.-]+)(:(?<port>\\d+)){0,1}$');\n    final RegExpMatch? match = exp.firstMatch(server);\n    if (match != null) {\n      scheme = match.namedGroup('scheme')!;\n      host = match.namedGroup('host')!;\n      port = match.namedGroup('port') != null\n          ? int.parse(match.namedGroup('port')!)\n          : null;\n    } else {\n      throw const FormatException('Invalid server format');\n    }\n\n    if (path == '/') {\n      this.path = path;\n    } else {\n      if (path.endsWith('/')) {\n        this.path = path.substring(0, path.length - 1);\n      } else {\n        this.path = path;\n      }\n    }\n  }\n\n  Future<webdav.Client> get connection async {\n    if (_client != null) {\n      return _client!;\n    } else {\n      _client = await _open();\n      return _client!;\n    }\n  }\n\n  Future<webdav.Client> _open() async {\n    // Handle untrusted certificates.\n    webdav.WdDio dio = webdav.WdDio(debug: false);\n    dio.httpClientAdapter = IOHttpClientAdapter(\n      createHttpClient: () {\n        // Don't trust any certificate just because their root cert is trusted.\n        final HttpClient httpClient =\n            HttpClient(context: SecurityContext(withTrustedRoots: false));\n        httpClient.idleTimeout = const Duration(\n          milliseconds: idleTimeout,\n        );\n        httpClient.connectionTimeout = const Duration(\n          milliseconds: connectionTimeout,\n        );\n        // You can test the intermediate / root cert here. We just ignore it.\n        httpClient.badCertificateCallback =\n            (cert, host, port) => acceptUntrustedCert;\n        return httpClient;\n      },\n      validateCertificate: (cert, host, port) => acceptUntrustedCert,\n    );\n\n    webdav.Client client = webdav.Client(\n      uri: Uri(\n        scheme: scheme,\n        host: host,\n        port: port,\n        path: path,\n      ).toString(),\n      c: dio,\n      auth: webdav.BasicAuth(user: username, pwd: password),\n      debug: false,\n    );\n    client.setHeaders({'accept-charset': 'utf-8'});\n    client.setConnectTimeout(connectionTimeout);\n    client.setSendTimeout(sendTimeout);\n    client.setReceiveTimeout(receiveTimeout);\n\n    return client;\n  }\n\n  (String, String) _handleDioError(DioException error) {\n    switch (error.type) {\n      case DioExceptionType.connectionTimeout:\n      case DioExceptionType.sendTimeout:\n      case DioExceptionType.receiveTimeout:\n        return (\n          'TIMEOUT',\n          'Timeout occurred while sending or receiving',\n        );\n      case DioExceptionType.badResponse:\n        final statusCode = error.response?.statusCode;\n        if (statusCode != null) {\n          switch (statusCode) {\n            case 400:\n              return (\n                'BAD REQUEST',\n                'Something went wrong',\n              );\n            case 401:\n              return (\n                'UNAUTHORIZED',\n                'It seems that the credentials are incorrect',\n              );\n            case 403:\n              return (\n                'FORBIDDEN',\n                'The request was rejected by the server',\n              );\n            case 404:\n              return (\n                'NOT FOUND',\n                'The requested resource could not be found',\n              );\n            case 405:\n              return (\n                'METHOD NOT ALLOWED',\n                'The request method is not supported for the requested resource',\n              );\n            case 409:\n              return (\n                'CONFLICT',\n                'The request could not be processed because of conflict in the current state of the resource',\n              );\n            case 500:\n              return (\n                'INTERNAL SERVER ERROR',\n                'An unexpected error was encountered on server side',\n              );\n            default:\n              return (\n                'INTERNAL SERVER ERROR ($statusCode)',\n                'An unexpected error was encountered on server side',\n              );\n          }\n        }\n        break;\n      case DioExceptionType.cancel:\n        break;\n      case DioExceptionType.badCertificate:\n        return (\n          'INVALID SSL CERTIFICATE',\n          'The certificate is invalid',\n        );\n      case DioExceptionType.connectionError:\n        return (\n          'CONNECTION ERROR',\n          'Server cannot be reached',\n        );\n      case DioExceptionType.unknown:\n        return (\n          'UNKNOWN ERROR',\n          'Possible cause may be a connection problem or an invalid SSL certificate',\n        );\n      default:\n        return (\n          'UNKNOWN ERROR',\n          'Possible cause may be a connection problem or an invalid SSL certificate',\n        );\n    }\n\n    return (\n      'UNKNOWN ERROR',\n      'Something unexpected went wrong',\n    );\n  }\n\n  Future<void> ping() async {\n    try {\n      log.fine('Ping');\n      webdav.Client client = await connection;\n      await client.ping();\n    } on DioException catch (e) {\n      // Handle 204 No Content as success\n      final statusCode = e.response?.statusCode;\n      if (statusCode == 204) {\n        log.fine('204 No Content (treated as success)');\n        return;\n      }\n      log.severe(e);\n      final (String, String) error = _handleDioError(e);\n      throw WebDAVClientException('${error.$1}: ${error.$2}');\n    } on Exception catch (e) {\n      log.severe(e);\n      throw const WebDAVClientException('Unknown Error: Something went wrong');\n    }\n  }\n\n  Future<bool> _exists({\n    required String path,\n    required String target,\n  }) async {\n    if (!path.startsWith('/')) {\n      path = '/$path';\n    }\n    if (!path.endsWith('/')) {\n      path = '$path/';\n    }\n    for (webdav.File f in await listFiles(path: path)) {\n      if (f.path == '$path$target') {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  Future<bool> fileExists({\n    required String filename,\n  }) async {\n    if (filename.startsWith('/')) {\n      filename = filename.substring(1);\n    }\n    if (filename.endsWith('/')) {\n      filename = filename.substring(0, filename.length - 1);\n    }\n    log.fine('Check if file $filename exists');\n    return await _exists(\n      path: p.dirname(filename),\n      target: p.basename(filename),\n    );\n  }\n\n  Future<webdav.File> getFile({\n    required String filename,\n  }) async {\n    webdav.Client client = await connection;\n    try {\n      log.fine('Read file object $filename');\n      return await client.readProps(filename);\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to read file object $filename from remote ${client.uri}',\n      );\n    }\n  }\n\n  Future<List<webdav.File>> listFiles({\n    String path = '',\n  }) async {\n    webdav.Client client = await connection;\n    try {\n      log.fine('List files and directories of ${path.isEmpty ? \"/\" : path}');\n      return await client.readDir(path);\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to list files in directory ${path.isEmpty ? \"/\" : path} on remote ${client.uri}',\n      );\n    }\n  }\n\n  Future<void> create(String filename) async {\n    webdav.Client client = await connection;\n    try {\n      if (filename.startsWith('/')) {\n        filename = filename.substring(1);\n      }\n      if (await fileExists(filename: filename) == false) {\n        // Create file by writing empty string.\n        log.fine('Create file $filename');\n        await client.write(filename, utf8.encode(''));\n      } else {\n        log.fine('Skip file creation. File $filename already exists');\n      }\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to create file $filename on remote ${client.uri}',\n      );\n    }\n  }\n\n  Future<void> mkdir(\n      {required String directory, bool recursive = false}) async {\n    webdav.Client client = await connection;\n    if (directory.startsWith('/')) {\n      directory = directory.substring(1);\n    }\n    if (directory.endsWith('/')) {\n      directory = directory.substring(0, directory.length - 1);\n    }\n    try {\n      log.fine('Create directory $directory');\n      if (recursive) {\n        await client.mkdirAll(directory);\n      } else {\n        await client.mkdir(directory);\n      }\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to create directory $directory on remote ${client.uri}',\n      );\n    }\n  }\n\n  Future<String> download({\n    required String filename,\n  }) async {\n    webdav.Client client = await connection;\n    try {\n      log.fine('Download content of file $filename');\n      List<int> content = await client.read(filename);\n      return utf8.decode(content);\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to download file $filename from remote ${client.uri}',\n      );\n    }\n  }\n\n  Future<void> upload({\n    required String filename,\n    required String content,\n  }) async {\n    webdav.Client client = await connection;\n    try {\n      log.fine('Upload content to file $filename');\n      await client.write(filename, utf8.encode(content));\n    } on Exception catch (e) {\n      log.severe(e);\n      throw WebDAVClientException(\n        'Failed to upload file $filename to remote ${client.uri}',\n      );\n    }\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 \"ntodotxt\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"de.tnmgl.ntodotxt\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# 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/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, \"ntodotxt\");\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, \"ntodotxt\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(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": "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 \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include \"ephemeral/Flutter-Generated.xcconfig\"\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"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\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 application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = ntodotxt\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2025 de.tnmgl. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.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.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC10EC2044A3C60003C045;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* ntodotxt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"ntodotxt.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\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t331C80D2294CF70F00263BE5 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\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);\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 /* ntodotxt.app */,\n\t\t\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\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);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C80D4294CF70F00263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C80D1294CF70F00263BE5 /* Sources */,\n\t\t\t\t331C80D2294CF70F00263BE5 /* Frameworks */,\n\t\t\t\t331C80D3294CF70F00263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\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);\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 /* ntodotxt.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\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C80D4294CF70F00263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 33CC10EC2044A3C60003C045;\n\t\t\t\t\t};\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t331C80D4294CF70F00263BE5 /* RunnerTests */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C80D3294CF70F00263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C80D1294CF70F00263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC10EC2044A3C60003C045 /* Runner */;\n\t\t\ttargetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t331C80DB294CF71000263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C80DC294CF71000263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C80DD294CF71000263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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.14;\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_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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.14;\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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = 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\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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.14;\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_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\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_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\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\t331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C80DB294CF71000263BE5 /* Debug */,\n\t\t\t\t331C80DC294CF71000263BE5 /* Release */,\n\t\t\t\t331C80DD294CF71000263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"ntodotxt.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 = \"ntodotxt.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      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"ntodotxt.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 = \"ntodotxt.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</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 Cocoa\nimport FlutterMacOS\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": "metadata/en-US/full_description.txt",
    "content": "With ntodotxt you can manage your todos in a todo.txt file (i.e. all information is stored in a single file). You can save your todos locally on your device and/or synchronize the todo.txt file via webdav - for example with a self-hosted nextcloud instance.\n"
  },
  {
    "path": "metadata/en-US/short_description.txt",
    "content": "Manage your todos in a todo.txt file\n"
  },
  {
    "path": "metadata/en-US/title.txt",
    "content": "ntodotxt\n"
  },
  {
    "path": "ntodotxt.yaml",
    "content": "session: ntodotxt\n\nroot: ~/Projekte/dart/flutter/ntodotxt/\n\nwindows:\n  - name: git\n  - name: neovim\n  - name: flutter run\n  - name: flutter test\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: ntodotxt\n\ndescription: App for managing your todos within a todo.txt file locally or via webdav (e.g. Nextcloud)\npublish_to: \"none\"\nversion: 0.17.0+39\n\nenvironment:\n  sdk: \">=3.0.5 <4.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  collection: ^1.17.2\n  crypto: ^3.0.3\n  cupertino_icons: ^1.0.6\n  dio: ^5.9.0\n  equatable: ^2.0.5\n  file_picker: ^10.1.9\n  flex_color_scheme: ^8.0.2\n  flutter_bloc: ^9.1.1\n  flutter_secure_storage: ^9.0.0\n  go_router: ^15.1.2\n  intl: any\n  logging: ^1.2.0\n  path: ^1.9.0\n  path_provider: ^2.0.11\n  permission_handler: ^12.0.0\n  rxdart: ^0.28.0\n  sqflite_common_ffi: ^2.3.1\n  sqlite3_flutter_libs: ^0.5.18\n  url_launcher: ^6.2.1\n  webdav_client: ^1.2.2\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  integration_test:\n    sdk: flutter\n  flutter_driver:\n    sdk: flutter\n  file: ^7.0.0\n  flutter_launcher_icons: ^0.14.3\n  flutter_lints: ^5.0.0\n  flutter_oss_licenses: ^3.0.4\n  test_cov_console: ^0.2.2\n\nflutter_launcher_icons:\n  android: true\n  image_path: assets/icon/icon.png\n  min_sdk_android: 21\n\nflutter:\n  uses-material-design: true\n  assets:\n    - assets/icon/icon.png\n  fonts:\n    - family: OpenSans\n      fonts:\n        - asset: fonts/OpenSans-VariableFont_wdth,wght.ttf\n        - asset: fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf\n"
  },
  {
    "path": "test/common/widget/confirm_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/confirm_dialog.dart';\n\nclass MaterialAppConfirmationDialog extends StatefulWidget {\n  const MaterialAppConfirmationDialog({super.key});\n\n  @override\n  State<MaterialAppConfirmationDialog> createState() =>\n      _MaterialAppConfirmationDialogState();\n}\n\nclass _MaterialAppConfirmationDialogState\n    extends State<MaterialAppConfirmationDialog> {\n  int _value = -1;\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            Text('$_value'),\n            Builder(\n              builder: (BuildContext context) {\n                return TextButton(\n                  child: const Text('Open dialog'),\n                  onPressed: () async {\n                    bool confirm = await ConfirmationDialog.dialog(\n                      context: context,\n                      title: 'Dialog',\n                      message: 'Question?',\n                      actionLabel: 'Ok',\n                      cancelLabel: 'Cancel',\n                    );\n\n                    setState(() {\n                      _value = confirm == true ? 1 : 0;\n                    });\n                  },\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('ConfirmationDialog', () {\n    testWidgets('confirm', (tester) async {\n      await tester.pumpWidget(const MaterialAppConfirmationDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(AlertDialog), findsOneWidget);\n      await tester.tap(find.text('Ok'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == '1',\n        ),\n        findsOneWidget,\n      );\n    });\n    testWidgets('confirm', (tester) async {\n      await tester.pumpWidget(const MaterialAppConfirmationDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(AlertDialog), findsOneWidget);\n      await tester.tap(find.text('Cancel'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == '0',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/contexts_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Todo;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterContextTagDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterContextTagDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(\n                          'result: ${state.filter.contexts.toString()}',\n                        ),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterContextTagDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                  availableTags: {\n                                    'context1',\n                                    'context2',\n                                    'context3'\n                                  },\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass MaterialAppTodoContextTagDialog extends StatelessWidget {\n  const MaterialAppTodoContextTagDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => TodoCubit(\n        todo: Todo(description: 'Test something'),\n      ),\n      child: Builder(\n        builder: (BuildContext context) {\n          return MaterialApp(\n            home: Scaffold(\n              body: BlocBuilder<TodoCubit, TodoState>(\n                builder: (BuildContext context, TodoState state) {\n                  return Column(\n                    children: [\n                      Text(\n                        'result: ${state.todo.contexts.toString()}',\n                      ),\n                      Builder(\n                        builder: (BuildContext context) {\n                          return TextButton(\n                            child: const Text('Open dialog'),\n                            onPressed: () async {\n                              await TodoContextTagDialog.dialog(\n                                context: context,\n                                cubit: BlocProvider.of<TodoCubit>(context),\n                                availableTags: {\n                                  'context1',\n                                  'context2',\n                                  'context3'\n                                },\n                              );\n                            },\n                          );\n                        },\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nFuture safeTapByFinder(WidgetTester tester, Finder finder) async {\n  await tester.ensureVisible(finder);\n  await tester.pumpAndSettle();\n  await tester.tap(finder);\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterContextTagDialog', () {\n    testWidgets('apply', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterContextTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterContextTagDialog')),\n          matching: find.text('context1'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {context1}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterContextTagDialog')),\n          matching: find.text('context1'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('TodoContextTagDialog', () {\n    testWidgets('enter', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoContextTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      Finder textField = find.descendant(\n        of: find.byKey(const Key('TodoContextTagDialog')),\n        matching: find.byType(TextFormField),\n      );\n      await tester.ensureVisible(textField);\n      await tester.pumpAndSettle();\n      await tester.enterText(textField, 'context99');\n      await tester.pumpAndSettle();\n      await safeTapByFinder(tester, find.text('Add'));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const Key('TodoContextTagDialog')),\n          matching: find.text('context99'),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {context99}',\n        ),\n        findsOneWidget,\n      );\n    });\n    testWidgets('enter (with leading @)', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoContextTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      Finder textField = find.descendant(\n        of: find.byKey(const Key('TodoContextTagDialog')),\n        matching: find.byType(TextFormField),\n      );\n      await tester.ensureVisible(textField);\n      await tester.pumpAndSettle();\n      await tester.enterText(textField, '@context99');\n      await tester.pumpAndSettle();\n      await safeTapByFinder(tester, find.text('Add'));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const Key('TodoContextTagDialog')),\n          matching: find.text('context99'),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {context99}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/default_filter_state_filter_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppDefaultFilterStateFilterDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppDefaultFilterStateFilterDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.filter.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await DefaultFilterStateFilterDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('DefaultFilterStateOrderDialog', () {\n    testWidgets('change', (tester) async {\n      await tester\n          .pumpWidget(const MaterialAppDefaultFilterStateFilterDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Completed only'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'completedOnly',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('All'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'all',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/default_filter_state_group_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppDefaultFilterStateGroupDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppDefaultFilterStateGroupDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.group.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await DefaultFilterStateGroupDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('DefaultFilterStateGroupDialog', () {\n    testWidgets('change', (tester) async {\n      await tester.pumpWidget(const MaterialAppDefaultFilterStateGroupDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Upcoming'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'upcoming',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('None'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'none',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/default_filter_state_order_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppDefaultFilterStateOrderDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppDefaultFilterStateOrderDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.order.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await DefaultFilterStateOrderDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('DefaultFilterStateOrderDialog', () {\n    testWidgets('change', (tester) async {\n      await tester.pumpWidget(const MaterialAppDefaultFilterStateOrderDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Descending'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'descending',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Ascending'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'ascending',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/filter_state_filter_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterStateFilterDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterStateFilterDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.filter.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterStateFilterDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterStateFilterDialog', () {\n    testWidgets('change', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterStateFilterDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Completed only'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'completedOnly',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('All'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'all',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/filter_state_group_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterStateGroupDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterStateGroupDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.group.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterStateGroupDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterStateGroupDialog', () {\n    testWidgets('change', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterStateGroupDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Upcoming'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'upcoming',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('None'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'none',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/filter_state_order_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterStateOrderDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterStateOrderDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(state.filter.order.name),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterStateOrderDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterStateOrderDialog', () {\n    testWidgets('change', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterStateOrderDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Descending'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'descending',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(Dialog), findsOneWidget);\n      await tester.tap(find.text('Ascending'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'ascending',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/info_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/info_dialog.dart';\n\nclass MaterialAppInfoDialog extends StatelessWidget {\n  const MaterialAppInfoDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            Builder(\n              builder: (BuildContext context) {\n                return TextButton(\n                  child: const Text('Open dialog'),\n                  onPressed: () async {\n                    await InfoDialog.dialog(\n                      context: context,\n                      title: 'Dialog title',\n                      message: 'Dialog text',\n                    );\n                  },\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('InfoDialog', () {\n    testWidgets('info', (tester) async {\n      await tester.pumpWidget(const MaterialAppInfoDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(AlertDialog), findsOneWidget);\n      expect(\n        find.descendant(\n          of: find.byType(AlertDialog),\n          matching: find.text('Dialog title'),\n        ),\n        findsOneWidget,\n      );\n      expect(\n        find.descendant(\n          of: find.byType(AlertDialog),\n          matching: find.text('Dialog text'),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/input_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/input_dialog.dart';\n\nclass MaterialAppInputDialog extends StatefulWidget {\n  const MaterialAppInputDialog({super.key});\n\n  @override\n  State<MaterialAppInputDialog> createState() => _MaterialAppInputDialogState();\n}\n\nclass _MaterialAppInputDialogState extends State<MaterialAppInputDialog> {\n  String _value = 'default';\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        body: Column(\n          children: [\n            Text(_value),\n            Builder(\n              builder: (BuildContext context) {\n                return TextButton(\n                  child: const Text('Open dialog'),\n                  onPressed: () async {\n                    final String? value = await InputDialog.dialog(\n                      context: context,\n                      title: 'Dialog',\n                      label: 'Enter value',\n                    );\n\n                    setState(() {\n                      _value = value ?? 'cancel';\n                    });\n                  },\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('InputDialog', () {\n    testWidgets('ok', (tester) async {\n      await tester.pumpWidget(const MaterialAppInputDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(AlertDialog), findsOneWidget);\n      await tester.enterText(find.byType(TextField), 'enter some text');\n      await tester.tap(find.text('Ok'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'enter some text',\n        ),\n        findsOneWidget,\n      );\n    });\n    testWidgets('cancel', (tester) async {\n      await tester.pumpWidget(const MaterialAppInputDialog());\n      await tester.pump();\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pump();\n\n      expect(find.byType(AlertDialog), findsOneWidget);\n      await tester.enterText(find.byType(TextField), 'enter some text');\n      await tester.tap(find.text('Cancel'));\n      await tester.pump();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'cancel',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/key_values_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/key_values_dialog.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Todo;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\n\nclass MaterialAppTodoKeyValueTagDialog extends StatelessWidget {\n  const MaterialAppTodoKeyValueTagDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => TodoCubit(\n        todo: Todo(description: 'Test something'),\n      ),\n      child: Builder(\n        builder: (BuildContext context) {\n          return MaterialApp(\n            home: Scaffold(\n              body: BlocBuilder<TodoCubit, TodoState>(\n                builder: (BuildContext context, TodoState state) {\n                  return Column(\n                    children: [\n                      Text(\n                        'result: ${state.todo.keyValues.toString()}',\n                      ),\n                      Builder(\n                        builder: (BuildContext context) {\n                          return TextButton(\n                            child: const Text('Open dialog'),\n                            onPressed: () async {\n                              await TodoKeyValueTagDialog.dialog(\n                                context: context,\n                                cubit: BlocProvider.of<TodoCubit>(context),\n                                availableTags: {\n                                  'key1:val1',\n                                  'key2:val2',\n                                  'key3:val3'\n                                },\n                              );\n                            },\n                          );\n                        },\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nFuture safeTapByFinder(WidgetTester tester, Finder finder) async {\n  await tester.ensureVisible(finder);\n  await tester.pumpAndSettle();\n  await tester.tap(finder);\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('TodoKeyValueTagDialog', () {\n    testWidgets('enter', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoKeyValueTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      Finder textField = find.descendant(\n        of: find.byKey(const Key('TodoKeyValueTagDialog')),\n        matching: find.byType(TextFormField),\n      );\n      await tester.ensureVisible(textField);\n      await tester.pumpAndSettle();\n      await tester.enterText(textField, 'foo:bar');\n      await tester.pumpAndSettle();\n      await safeTapByFinder(tester, find.text('Add'));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const Key('TodoKeyValueTagDialog')),\n          matching: find.text('foo:bar'),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {foo:bar}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/priorities_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterPriorityTagDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterPriorityTagDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(\n                          'result: ${state.filter.priorities.toString()}',\n                        ),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterPriorityTagDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                  availableTags: {\n                                    Priority.A,\n                                    Priority.B,\n                                    Priority.C\n                                  },\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass MaterialAppTodoPriorityTagDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppTodoPriorityTagDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => TodoCubit(\n          todo: Todo(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<TodoCubit, TodoState>(\n                  builder: (BuildContext context, TodoState state) {\n                    return Column(\n                      children: [\n                        Text(\n                          'result: ${state.todo.priority.toString()}',\n                        ),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await TodoPriorityTagDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<TodoCubit>(context),\n                                  availableTags: {\n                                    Priority.A,\n                                    Priority.B,\n                                    Priority.C\n                                  },\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nFuture safeTapByFinder(WidgetTester tester, Finder finder) async {\n  await tester.ensureVisible(finder);\n  await tester.pumpAndSettle();\n  await tester.tap(finder);\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterPriorityTagDialog', () {\n    testWidgets('set & unset', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterPriorityTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterPriorityTagDialog')),\n          matching: find.text('A'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {Priority.A}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterPriorityTagDialog')),\n          matching: find.text('A'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('TodoPriorityTagDialog', () {\n    testWidgets('set & unset', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoPriorityTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: Priority.none',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('TodoPriorityTagDialog')),\n          matching: find.text('A'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: Priority.A',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('TodoPriorityTagDialog')),\n          matching: find.text('A'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: Priority.none',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/common/widget/projects_dialog_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Todo;\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppFilterProjectTagDialog extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppFilterProjectTagDialog({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: const Filter(),\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return MaterialApp(\n              home: Scaffold(\n                body: BlocBuilder<FilterCubit, FilterState>(\n                  builder: (BuildContext context, FilterState state) {\n                    return Column(\n                      children: [\n                        Text(\n                          'result: ${state.filter.projects.toString()}',\n                        ),\n                        Builder(\n                          builder: (BuildContext context) {\n                            return TextButton(\n                              child: const Text('Open dialog'),\n                              onPressed: () async {\n                                await FilterProjectTagDialog.dialog(\n                                  context: context,\n                                  cubit: BlocProvider.of<FilterCubit>(context),\n                                  availableTags: {\n                                    'project1',\n                                    'project2',\n                                    'project3'\n                                  },\n                                );\n                              },\n                            );\n                          },\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass MaterialAppTodoProjectTagDialog extends StatelessWidget {\n  const MaterialAppTodoProjectTagDialog({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (BuildContext context) => TodoCubit(\n        todo: Todo(description: 'Test something'),\n      ),\n      child: Builder(\n        builder: (BuildContext context) {\n          return MaterialApp(\n            home: Scaffold(\n              body: BlocBuilder<TodoCubit, TodoState>(\n                builder: (BuildContext context, TodoState state) {\n                  return Column(\n                    children: [\n                      Text(\n                        'result: ${state.todo.projects.toString()}',\n                      ),\n                      Builder(\n                        builder: (BuildContext context) {\n                          return TextButton(\n                            child: const Text('Open dialog'),\n                            onPressed: () async {\n                              await TodoProjectTagDialog.dialog(\n                                context: context,\n                                cubit: BlocProvider.of<TodoCubit>(context),\n                                availableTags: {\n                                  'project1',\n                                  'project2',\n                                  'project3'\n                                },\n                              );\n                            },\n                          );\n                        },\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nFuture safeTapByFinder(WidgetTester tester, Finder finder) async {\n  await tester.ensureVisible(finder);\n  await tester.pumpAndSettle();\n  await tester.tap(finder);\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('FilterProjectTagDialog', () {\n    testWidgets('apply', (tester) async {\n      await tester.pumpWidget(const MaterialAppFilterProjectTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterProjectTagDialog')),\n          matching: find.text('project1'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {project1}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      await safeTapByFinder(\n        tester,\n        find.descendant(\n          of: find.byKey(const Key('FilterProjectTagDialog')),\n          matching: find.text('project1'),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('TodoProjectTagDialog', () {\n    testWidgets('enter', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoProjectTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      Finder textField = find.descendant(\n        of: find.byKey(const Key('TodoProjectTagDialog')),\n        matching: find.byType(TextFormField),\n      );\n      await tester.ensureVisible(textField);\n      await tester.pumpAndSettle();\n      await tester.enterText(textField, 'project99');\n      await tester.pumpAndSettle();\n      await safeTapByFinder(tester, find.text('Add'));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const Key('TodoProjectTagDialog')),\n          matching: find.text('project99'),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {project99}',\n        ),\n        findsOneWidget,\n      );\n    });\n    testWidgets('enter (with leading +)', (tester) async {\n      await tester.pumpWidget(const MaterialAppTodoProjectTagDialog());\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) => widget is Text && widget.data == 'result: {}',\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.text('Open dialog'));\n      await tester.pumpAndSettle();\n\n      Finder textField = find.descendant(\n        of: find.byKey(const Key('TodoProjectTagDialog')),\n        matching: find.byType(TextFormField),\n      );\n      await tester.ensureVisible(textField);\n      await tester.pumpAndSettle();\n      await tester.enterText(textField, '+project99');\n      await tester.pumpAndSettle();\n      await safeTapByFinder(tester, find.text('Add'));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const Key('TodoProjectTagDialog')),\n          matching: find.text('project99'),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byWidgetPredicate(\n          (Widget widget) =>\n              widget is Text && widget.data == 'result: {project99}',\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/drawer/state/drawer_cubit_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/drawer/state/drawer_cubit.dart';\nimport 'package:ntodotxt/drawer/state/drawer_state.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() async {});\n\n  group('initial', () {\n    test('initial state', () {\n      final DrawerCubit cubit = DrawerCubit();\n      expect(cubit.state, const DrawerState(index: 0));\n    });\n  });\n\n  group('next()', () {\n    test('call one time next()', () {\n      final DrawerCubit cubit = DrawerCubit();\n      cubit.next(9);\n      expect(cubit.state, const DrawerState(index: 9, prevIndex: 0));\n    });\n    test('call two times next()', () {\n      final DrawerCubit cubit = DrawerCubit();\n      cubit.next(9);\n      cubit.next(2);\n      expect(cubit.state, const DrawerState(index: 2, prevIndex: 9));\n    });\n  });\n\n  group('back()', () {\n    test('call two times next() and one time back()', () {\n      final DrawerCubit cubit = DrawerCubit();\n      cubit.next(9);\n      cubit.next(2);\n      cubit.back();\n      expect(cubit.state, const DrawerState(index: 9, prevIndex: null));\n    });\n    test('call two times next() and two time back()', () {\n      final DrawerCubit cubit = DrawerCubit();\n      cubit.next(9);\n      cubit.next(2);\n      cubit.back();\n      cubit.back();\n      expect(cubit.state, const DrawerState(index: 0, prevIndex: null));\n    });\n  });\n\n  group('reset()', () {\n    test('call two times next() and one time reset()', () {\n      final DrawerCubit cubit = DrawerCubit();\n      cubit.next(9);\n      cubit.next(2);\n      cubit.reset();\n      expect(cubit.state, const DrawerState(index: 0, prevIndex: null));\n    });\n  });\n\n  group('toString()', () {\n    test('default', () {\n      const DrawerState state = DrawerState(index: 99, prevIndex: 0);\n      expect(\n        state.toString(),\n        'DrawerState { index: 99 prevIndex: 0 }',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/filter/controller/filter_controller_test.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart'\n    show FilterController;\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart'\n    show FilterRepository;\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  // Init ffi loader if needed.\n  sqfliteFfiInit();\n\n  late DatabaseController controller;\n  late FilterRepository repository;\n\n  setUp(() async {\n    controller = DatabaseController(inMemoryDatabasePath);\n    repository = FilterRepository(FilterController(controller));\n    await (await controller.database).delete('filters'); // Clear\n  });\n\n  group('list()', () {\n    test('empty', () async {\n      expect(await repository.list(), isEmpty);\n    });\n    test('filled', () async {\n      Filter model = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      await (await controller.database).insert('filters', model.toMap());\n      expect(await repository.list(), [model]);\n    });\n  });\n\n  group('get()', () {\n    test('empty', () async {\n      expect(await repository.get(id: 1), null);\n    });\n    test('filled', () async {\n      Filter model = const Filter(\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      await (await controller.database).insert('filters', model.toMap());\n      expect(await repository.get(id: 1), model.copyWith(id: 1));\n    });\n  });\n\n  group('insert()', () {\n    test('empty', () async {\n      Filter model = const Filter(\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      expect(await repository.insert(model) > 0, isTrue);\n    });\n    test('filled', () async {\n      Filter model = const Filter(\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      await (await controller.database).insert('filters', model.toMap());\n      expect(await repository.insert(model) > 0, isTrue);\n    });\n    test('ignore id', () async {\n      Filter model = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      expect(await repository.insert(model) > 0, isTrue);\n    });\n  });\n\n  group('update()', () {\n    test('empty', () async {\n      Filter model = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      expect(await repository.update(model), 0);\n    });\n    test('filled', () async {\n      Filter model1 = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      Filter model2 = model1.copyWith(name: 'updated name');\n      await (await controller.database).insert('filters', model1.toMap());\n      expect(await repository.update(model2) > 0, isTrue);\n    });\n    test('missing id', () async {\n      Filter model = const Filter(\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      expect(await repository.update(model), 0);\n    });\n  });\n\n  group('delete()', () {\n    test('empty', () async {\n      Filter model = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      expect(await repository.delete(id: model.id!), 0);\n    });\n    test('filled', () async {\n      Filter model = const Filter(\n        id: 1,\n        name: 'example filter',\n        priorities: {Priority.A, Priority.B},\n        projects: {'project1', 'project2'},\n        contexts: {'context1', 'context2'},\n        order: ListOrder.ascending,\n        filter: ListFilter.all,\n        group: ListGroup.priority,\n      );\n      await (await controller.database).insert('filters', model.toMap());\n      expect(await repository.delete(id: model.id!) > 0, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "test/filter/page/filter_create_edit_page_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/page/filter_create_edit_page.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass BlocRepositoryWrapper extends StatelessWidget {\n  final Filter? initFilter;\n  final Set<String> projects;\n  final Set<String> contexts;\n\n  const BlocRepositoryWrapper({\n    this.initFilter,\n    this.projects = const {},\n    this.contexts = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final DatabaseController dbController =\n        DatabaseController(inMemoryDatabasePath);\n\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: MaterialApp(\n        home: FilterCreateEditPage(\n          initFilter: initFilter,\n          projects: projects,\n          contexts: contexts,\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  group('FilterCreateEditPage', () {\n    group('narrow view', () {\n      group('create mode', () {\n        testWidgets('found no SaveFilterIconButton if name is empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveFilterIconButton if name is not empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          await tester.enterText(find.byType(TextFormField), 'Filter name');\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no DeleteFilterIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteFilterIconButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n\n      group('edit mode', () {\n        testWidgets(\n            'found no SaveFilterIconButton if filter has not be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveFilterIconButton if filter has be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          await tester.dragUntilVisible(\n            find.byType(FilterOrderItem),\n            find.byType(ListView),\n            const Offset(0, -100),\n          );\n          await tester.tap(find.byType(FilterOrderItem));\n          await tester.pumpAndSettle();\n          await tester.ensureVisible(find.byType(FilterStateOrderDialog));\n          await tester.pumpAndSettle();\n          await tester.tap(\n            find.descendant(\n              of: find.byType(FilterStateOrderDialog),\n              matching: find.text('Descending'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(FilterOrderItem),\n              matching: find.byWidgetPredicate(\n                (Widget widget) =>\n                    widget is BasicChip &&\n                    widget.label == ListOrder.descending.name,\n              ),\n            ),\n            findsOneWidget,\n          );\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DeleteFilterIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteFilterIconButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n    });\n\n    group('wide view', () {\n      group('create mode', () {\n        testWidgets('found no SaveFilterIconButton if name is empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveFilterIconButton if name is not empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          await tester.enterText(find.byType(TextFormField), 'Filter name');\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no DeleteFilterIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const BlocRepositoryWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteFilterIconButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n      group('edit mode', () {\n        testWidgets(\n            'found no SaveFilterIconButton if filter has not be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveFilterIconButton if filter has be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          await tester.dragUntilVisible(\n            find.byType(FilterOrderItem),\n            find.byType(ListView),\n            const Offset(0, -100),\n          );\n          await tester.tap(find.byType(FilterOrderItem));\n          await tester.pumpAndSettle();\n          await tester.ensureVisible(find.byType(FilterStateOrderDialog));\n          await tester.pumpAndSettle();\n          await tester.tap(\n            find.descendant(\n              of: find.byType(FilterStateOrderDialog),\n              matching: find.text('Descending'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(FilterOrderItem),\n              matching: find.byWidgetPredicate(\n                (Widget widget) =>\n                    widget is BasicChip &&\n                    widget.label == ListOrder.descending.name,\n              ),\n            ),\n            findsOneWidget,\n          );\n          expect(\n            find.descendant(\n              of: find.byType(SaveFilterIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DeleteFilterIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            BlocRepositoryWrapper(\n              initFilter: const Filter().copyWith(name: 'filter'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteFilterIconButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n    });\n\n    group('default values', () {\n      testWidgets('FilterOrderItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterOrderItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterOrderItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListOrder.ascending.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterFilterItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterFilterItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterFilterItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == ListFilter.all.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterGroupItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterGroupItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterGroupItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == ListGroup.none.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterPrioritiesItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterPrioritiesItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterPrioritiesItem),\n            matching: find.byType(BasicChip),\n          ),\n          findsNothing,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterPrioritiesItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) => widget is Text && widget.data == '-',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterProjectTagsItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterProjectTagsItem),\n            matching: find.byType(BasicChip),\n          ),\n          findsNothing,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) => widget is Text && widget.data == '-',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterContextTagsItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterContextTagsItem),\n            matching: find.byType(BasicChip),\n          ),\n          findsNothing,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) => widget is Text && widget.data == '-',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('non default values', () {\n      testWidgets('FilterNameTextField', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(name: 'filter name'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterNameTextField),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterNameTextField),\n            matching: find.text('filter name'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterOrderItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(order: ListOrder.descending),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterOrderItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterOrderItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListOrder.descending.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterFilterItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter:\n                const Filter().copyWith(filter: ListFilter.completedOnly),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterFilterItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterFilterItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListFilter.completedOnly.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterGroupItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(group: ListGroup.project),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterGroupItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterGroupItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == ListGroup.project.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterPrioritiesItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(\n              priorities: {Priority.A, Priority.B},\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterPrioritiesItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterPrioritiesItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == Priority.A.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterPrioritiesItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == Priority.B.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterProjectTagsItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(\n              projects: {'project1', 'project2'},\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project1',\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project2',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterContextTagsItem', (tester) async {\n        await tester.pumpWidget(\n          BlocRepositoryWrapper(\n            initFilter: const Filter().copyWith(\n              contexts: {'context1', 'context2'},\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context1',\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(FilterContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context2',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('update values', () {\n      testWidgets('FilterOrderItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterOrderItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterOrderItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterStateOrderDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterStateOrderDialog),\n            matching: find.text('Descending'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterOrderItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListOrder.descending.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterFilterItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterFilterItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterFilterItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterStateFilterDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterStateFilterDialog),\n            matching: find.text('Completed only'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterFilterItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListFilter.completedOnly.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterGroupItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterGroupItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterGroupItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterStateGroupDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterStateGroupDialog),\n            matching: find.text('Upcoming'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterGroupItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip &&\n                  widget.label == ListGroup.upcoming.name,\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterPrioritiesItem', (tester) async {\n        await tester.pumpWidget(const BlocRepositoryWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterPrioritiesItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterPrioritiesItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterPriorityTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterPriorityTagDialog),\n            matching: find.text('A'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterPrioritiesItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) => widget is BasicChip && widget.label == 'A',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterProjectTagsItem', (tester) async {\n        await tester.pumpWidget(\n          const BlocRepositoryWrapper(\n            projects: {'project1', 'project2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterProjectTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterProjectTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterProjectTagDialog),\n            matching: find.text('project1'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('FilterContextTagsItem', (tester) async {\n        await tester.pumpWidget(\n          const BlocRepositoryWrapper(\n            contexts: {'context1', 'context2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(FilterContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(FilterContextTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(FilterContextTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(FilterContextTagDialog),\n            matching: find.text('context1'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(FilterContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/filter/page/filter_list_page_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart' show Filter;\nimport 'package:ntodotxt/filter/page/filter_list_page.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingController;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass FakeController extends Fake implements FilterController {\n  List<Filter> items = [\n    const Filter(id: 1, name: 'Filter 01'),\n    const Filter(id: 2, name: 'Filter 02'),\n    const Filter(id: 3, name: 'Filter 03'),\n  ];\n\n  @override\n  Future<List<Filter>> list() async {\n    return Future.value(items);\n  }\n}\n\nclass FilterListPageMaterialApp extends StatelessWidget {\n  final DatabaseController dbController =\n      const DatabaseController(inMemoryDatabasePath);\n  final FilterController controller;\n\n  const FilterListPageMaterialApp({\n    required this.controller,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) =>\n              SettingRepository(SettingController(dbController)),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(controller),\n        ),\n        BlocProvider<InteractionSettingsCubit>(\n          create: (BuildContext context) => InteractionSettingsCubit(\n            repository: context.read<SettingRepository>(),\n          ),\n        ),\n      ],\n      child: Builder(\n        builder: (BuildContext context) {\n          return MultiBlocProvider(\n            providers: [\n              BlocProvider<FilterListBloc>(\n                create: (BuildContext context) {\n                  return FilterListBloc(\n                    repository: context.read<FilterRepository>(),\n                  )\n                    ..add(const FilterListSubscriped())\n                    ..add(const FilterListSynchronizationRequested());\n                },\n              ),\n            ],\n            child: const MaterialApp(\n              home: FilterListPage(),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('List', () {\n    testWidgets('narrow view', (tester) async {\n      // Increase size to ensure all elements in list are visible.\n      tester.view.physicalSize = const Size(400, 800);\n      tester.view.devicePixelRatio = 1.0;\n\n      await tester.pumpWidget(FilterListPageMaterialApp(\n        controller: FakeController(),\n      ));\n      await tester.pump();\n\n      expect(find.byType(FilterListTile), findsNWidgets(3));\n      Iterable<FilterListTile> filterTiles =\n          tester.widgetList<FilterListTile>(find.byType(FilterListTile));\n      expect(filterTiles.elementAt(0).filter.name, 'Filter 01');\n      expect(filterTiles.elementAt(1).filter.name, 'Filter 02');\n      expect(filterTiles.elementAt(2).filter.name, 'Filter 03');\n\n      // resets the screen to its original size after the test end\n      addTearDown(tester.view.resetPhysicalSize);\n      addTearDown(tester.view.resetDevicePixelRatio);\n    });\n    testWidgets('wide view', (tester) async {\n      // Increase size to ensure all elements in list are visible.\n      tester.view.physicalSize = const Size(800, 1200);\n      tester.view.devicePixelRatio = 1.0;\n\n      await tester.pumpWidget(FilterListPageMaterialApp(\n        controller: FakeController(),\n      ));\n      await tester.pump();\n\n      expect(find.byType(FilterListTile), findsNWidgets(3));\n      Iterable<FilterListTile> filterTiles =\n          tester.widgetList<FilterListTile>(find.byType(FilterListTile));\n      expect(filterTiles.elementAt(0).filter.name, 'Filter 01');\n      expect(filterTiles.elementAt(1).filter.name, 'Filter 02');\n      expect(filterTiles.elementAt(2).filter.name, 'Filter 03');\n\n      // resets the screen to its original size after the test end\n      addTearDown(tester.view.resetPhysicalSize);\n      addTearDown(tester.view.resetDevicePixelRatio);\n    });\n  });\n}\n"
  },
  {
    "path": "test/filter/state/filter_cubit_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show Filter, ListFilter, ListGroup, ListOrder;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart' show Priority;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  late DatabaseController controller;\n  late SettingRepository settingRepository;\n  late FilterRepository filterRepository;\n\n  setUp(() {\n    controller = DatabaseController(inMemoryDatabasePath);\n    settingRepository = SettingRepository(\n      SettingController(controller),\n    );\n    filterRepository = FilterRepository(\n      FilterController(controller),\n    );\n  });\n\n  group('saved filter', () {\n    group('initial', () {\n      test('initial filter', () async {\n        const Filter origin = Filter(name: 'default');\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter(\n              name: 'default',\n              order: ListOrder.ascending,\n              filter: ListFilter.all,\n              group: ListGroup.none,\n            ),\n            origin: origin,\n          ),\n        );\n      });\n    });\n\n    group('create filter', () {\n      test('non-existing', () async {\n        const Filter origin = Filter(name: 'default');\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.create(origin.copyWith(name: 'created'));\n\n        await expectLater(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter(\n              id: 1,\n              name: 'created',\n              order: ListOrder.ascending,\n              filter: ListFilter.all,\n              group: ListGroup.none,\n            ),\n            origin: const Filter(\n              id: 1,\n              name: 'created',\n              order: ListOrder.ascending,\n              filter: ListFilter.all,\n              group: ListGroup.none,\n            ),\n          ),\n        );\n      });\n    });\n\n    // @todo: Fix testcase\n    // group('update filter', () {\n    //   test('existing', () async {\n    //     const Filter origin = Filter(name: 'default');\n    //     final FilterCubit cubit = FilterCubit(\n    //       settingRepository: settingRepository,\n    //       filterRepository: filterRepository,\n    //       filter: origin,\n    //     );\n    //     await cubit.create(origin);\n    //     await cubit.update(origin.copyWith(id: 1, name: 'updated'));\n    //\n    //     await expectLater(\n    //       cubit.state,\n    //       FilterSaved(\n    //         filter: const Filter(\n    //           id: 1,\n    //           name: 'updated',\n    //           order: ListOrder.ascending,\n    //           filter: ListFilter.all,\n    //           group: ListGroup.none,\n    //         ),\n    //         origin: const Filter(\n    //           id: 1,\n    //           name: 'updated',\n    //           order: ListOrder.ascending,\n    //           filter: ListFilter.all,\n    //           group: ListGroup.none,\n    //         ),\n    //       ),\n    //     );\n    //   });\n    // });\n\n    group('delete filter', () {\n      test('existing', () async {\n        const Filter origin = Filter(name: 'default');\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.create(origin);\n        await cubit.delete(origin.copyWith(id: 1, name: 'deleted'));\n\n        await expectLater(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter(),\n            origin: const Filter(),\n          ),\n        );\n      });\n    });\n\n    group('update attributes', () {\n      test('name', () async {\n        const Filter origin = Filter(name: 'default');\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateName('update');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(name: 'update'),\n            origin: origin,\n          ),\n        );\n      });\n      test('order', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateOrder(ListOrder.descending);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(order: ListOrder.descending),\n            origin: origin,\n          ),\n        );\n      });\n      test('filter', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateFilter(ListFilter.completedOnly);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(filter: ListFilter.completedOnly),\n            origin: origin,\n          ),\n        );\n      });\n      test('group', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateGroup(ListGroup.priority);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(group: ListGroup.priority),\n            origin: origin,\n          ),\n        );\n      });\n    });\n\n    group('priority', () {\n      test('add', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addPriority(Priority.A);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(priorities: {Priority.A}),\n            origin: origin,\n          ),\n        );\n      });\n      test('add (already exists)', () async {\n        const Filter origin = Filter(priorities: {Priority.A});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addPriority(Priority.A);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(priorities: {Priority.A}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove', () async {\n        const Filter origin = Filter(priorities: {Priority.A});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removePriority(Priority.A);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(priorities: {}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove (not exists)', () async {\n        const Filter origin = Filter(priorities: {Priority.A});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removePriority(Priority.B);\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(priorities: {Priority.A}),\n            origin: origin,\n          ),\n        );\n      });\n      test('update multiple', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updatePriorities({Priority.A, Priority.B});\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(\n              priorities: {Priority.A, Priority.B},\n            ),\n            origin: origin,\n          ),\n        );\n      });\n    });\n\n    group('project', () {\n      test('add', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addProject('project1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(projects: {'project1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('add (already exists)', () async {\n        const Filter origin = Filter(projects: {'project1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addProject('project1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(projects: {'project1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove', () async {\n        const Filter origin = Filter(projects: {'project1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removeProject('project1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(projects: {}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove (not exists)', () async {\n        const Filter origin = Filter(projects: {'project1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removeProject('project2');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(projects: {'project1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('update multiple', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateProjects({'project1', 'project2'});\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(projects: {'project1', 'project2'}),\n            origin: origin,\n          ),\n        );\n      });\n    });\n\n    group('context', () {\n      test('add', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addContext('context1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(contexts: {'context1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('add (already exists)', () async {\n        const Filter origin = Filter(contexts: {'context1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.addContext('context1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(contexts: {'context1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove', () async {\n        const Filter origin = Filter(contexts: {'context1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removeContext('context1');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(contexts: {}),\n            origin: origin,\n          ),\n        );\n      });\n      test('remove (not exists)', () async {\n        const Filter origin = Filter(contexts: {'context1'});\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.removeContext('context2');\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(contexts: {'context1'}),\n            origin: origin,\n          ),\n        );\n      });\n      test('update multiple', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        cubit.updateContexts({'context1', 'context2'});\n\n        expect(\n          cubit.state,\n          FilterChanged(\n            filter: const Filter().copyWith(contexts: {'context1', 'context2'}),\n            origin: origin,\n          ),\n        );\n      });\n    });\n  });\n\n  group('default filter', () {\n    group('update', () {\n      test('order', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.updateDefaultOrder(ListOrder.descending);\n\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter().copyWith(order: ListOrder.descending),\n            origin: const Filter().copyWith(order: ListOrder.descending),\n          ),\n        );\n      });\n      test('filter', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.updateDefaultFilter(ListFilter.completedOnly);\n\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter().copyWith(filter: ListFilter.completedOnly),\n            origin: const Filter().copyWith(filter: ListFilter.completedOnly),\n          ),\n        );\n      });\n      test('group', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.updateDefaultGroup(ListGroup.priority);\n\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter().copyWith(group: ListGroup.priority),\n            origin: const Filter().copyWith(group: ListGroup.priority),\n          ),\n        );\n      });\n    });\n\n    group('reset', () {\n      test('full', () async {\n        const Filter origin = Filter();\n        final FilterCubit cubit = FilterCubit(\n          settingRepository: settingRepository,\n          filterRepository: filterRepository,\n          filter: origin,\n        );\n        await cubit.updateDefaultOrder(ListOrder.descending);\n        await cubit.updateDefaultFilter(ListFilter.completedOnly);\n        await cubit.updateDefaultGroup(ListGroup.priority);\n\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: const Filter(\n              order: ListOrder.descending,\n              filter: ListFilter.completedOnly,\n              group: ListGroup.priority,\n            ),\n            origin: const Filter(\n              order: ListOrder.descending,\n              filter: ListFilter.completedOnly,\n              group: ListGroup.priority,\n            ),\n          ),\n        );\n\n        await cubit.resetToDefaults();\n        expect(\n          cubit.state,\n          FilterSaved(\n            filter: origin,\n            origin: origin,\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/filter/widget/filter_chip_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/filter_dialog.dart';\nimport 'package:ntodotxt/common/widget/group_by_dialog.dart';\nimport 'package:ntodotxt/common/widget/order_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart';\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_state.dart';\nimport 'package:ntodotxt/filter/widget/filter_chip.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass BlocAppWrapper extends StatelessWidget {\n  final Widget child;\n  final Filter filter;\n  final DatabaseController dbController =\n      const DatabaseController(inMemoryDatabasePath);\n\n  const BlocAppWrapper({\n    required this.child,\n    required this.filter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider<FilterCubit>(\n        create: (BuildContext context) => FilterCubit(\n          settingRepository: context.read<SettingRepository>(),\n          filterRepository: context.read<FilterRepository>(),\n          filter: filter,\n        ),\n        child: Builder(\n          builder: (BuildContext context) {\n            return BlocBuilder<FilterCubit, FilterState>(\n              builder: (BuildContext context, FilterState state) {\n                return MaterialApp(home: Scaffold(body: child));\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  group('FilterOrderChip', () {\n    testWidgets('ascending', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            order: ListOrder.ascending,\n          ),\n          child: FilterOrderChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('asc'), findsOneWidget);\n      expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('descending', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            order: ListOrder.descending,\n          ),\n          child: FilterOrderChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('desc'), findsOneWidget);\n      expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('ascending changed', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            order: ListOrder.ascending,\n          ),\n          child: FilterOrderChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterOrderChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterStateOrderDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('Descending'));\n      await tester.pumpAndSettle();\n\n      expect(find.text('desc'), findsOneWidget);\n      expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n    testWidgets('descending changed', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            order: ListOrder.descending,\n          ),\n          child: FilterOrderChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterOrderChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterStateOrderDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('Ascending'));\n      await tester.pumpAndSettle();\n\n      expect(find.text('asc'), findsOneWidget);\n      expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('FilterFilterChip', () {\n    testWidgets('all', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            filter: ListFilter.all,\n          ),\n          child: FilterFilterChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('all'), findsOneWidget);\n      expect(find.byIcon(Icons.filter_list), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('completed', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            filter: ListFilter.completedOnly,\n          ),\n          child: FilterFilterChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('completed'), findsOneWidget);\n      expect(find.byIcon(Icons.done_all), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('incompleted', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            filter: ListFilter.incompletedOnly,\n          ),\n          child: FilterFilterChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('incompleted'), findsOneWidget);\n      expect(find.byIcon(Icons.remove_done), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('update', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            filter: ListFilter.completedOnly,\n          ),\n          child: FilterFilterChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterFilterChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterStateFilterDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('All'));\n      await tester.pumpAndSettle();\n\n      expect(find.text('all'), findsOneWidget);\n      expect(find.byIcon(Icons.filter_list), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('FilterGroupChip', () {\n    testWidgets('none', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.none,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('none'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('upcoming', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.upcoming,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('upcoming'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('priority', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.priority,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('priority'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('project', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.project,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('project'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('context', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.context,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('context'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('update', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            group: ListGroup.none,\n          ),\n          child: FilterGroupChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterGroupChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterStateGroupDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('Priority'));\n      await tester.pumpAndSettle();\n\n      expect(find.text('priority'), findsOneWidget);\n      expect(find.byIcon(Icons.workspaces), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('FilterPrioritiesChip', () {\n    testWidgets('default', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            priorities: {},\n          ),\n          child: FilterPrioritiesChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('priorities'), findsOneWidget);\n      expect(find.byIcon(Icons.flag_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('update', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            priorities: {Priority.A},\n          ),\n          child: FilterPrioritiesChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterPrioritiesChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterPriorityTagDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('A'));\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(find.text('priorities'), findsOneWidget);\n      expect(find.byIcon(Icons.flag), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('FilterProjectsChip', () {\n    testWidgets('default', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            projects: {},\n          ),\n          child: FilterProjectsChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('projects'), findsOneWidget);\n      expect(find.byIcon(Icons.rocket_launch_outlined), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('update', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            projects: {'project1', 'project2'},\n          ),\n          child: FilterProjectsChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterProjectsChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterProjectTagDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('project2'));\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(find.text('projects'), findsOneWidget);\n      expect(find.byIcon(Icons.rocket_launch), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n\n  group('FilterContextsChip', () {\n    testWidgets('default', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            contexts: {},\n          ),\n          child: FilterContextsChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('contexts'), findsOneWidget);\n      expect(find.byIcon(Icons.tag), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == false),\n        findsOneWidget,\n      );\n    });\n    testWidgets('update', (tester) async {\n      await tester.pumpWidget(\n        const BlocAppWrapper(\n          filter: Filter(\n            contexts: {'context1', 'context2'},\n          ),\n          child: FilterContextsChip(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(find.byType(FilterContextsChip));\n      await tester.pumpAndSettle();\n      await tester.ensureVisible(find.byType(FilterContextTagDialog));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text('context2'));\n      await tester.pumpAndSettle();\n      await tester.drag(find.byType(DraggableScrollableSheet),\n          const Offset(0, 500)); // Dismiss dialog.\n      await tester.pumpAndSettle();\n\n      expect(find.text('contexts'), findsOneWidget);\n      expect(find.byIcon(Icons.tag), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((Widget widget) =>\n            widget is GenericActionChip && widget.selected == true),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "test/login/page/webdav_login_view_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/login/page/login_page.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass MaterialAppWebDAVLoginView extends StatelessWidget {\n  final DatabaseController dbController;\n\n  const MaterialAppWebDAVLoginView({\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n      ],\n      child: BlocProvider(\n        create: (BuildContext context) => TodoFileCubit(\n          repository: context.read<SettingRepository>(),\n          state: TodoFileReady(),\n        )..load(),\n        child: Builder(\n          builder: (BuildContext context) {\n            return const MaterialApp(\n              debugShowCheckedModeBanner: false,\n              home: WebDAVLoginView(),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  group('Login form validation', () {\n    group('success', () {\n      testWidgets('Render form', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        expect(find.byType(WebDAVLoginView), findsOneWidget);\n      });\n      testWidgets('Server address with http', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'http://localhost');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsNothing,\n        );\n      });\n      testWidgets('Server address with https', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'https://localhost');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsNothing,\n        );\n      });\n      testWidgets('Server address with port', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'http://localhost:80');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsNothing,\n        );\n      });\n      testWidgets('Server address with dots', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'http://localhost.local');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsNothing,\n        );\n      });\n    });\n\n    group('failed', () {\n      testWidgets('Missing path', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.text('Missing path'), findsOneWidget);\n      });\n      testWidgets('Missing username', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.text('Missing username'), findsOneWidget);\n      });\n      testWidgets('Missing password', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.text('Missing password'), findsOneWidget);\n      });\n      testWidgets('Missing server address', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(find.text('Missing server address'), findsOneWidget);\n      });\n      testWidgets('Missing protocol', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'localhost');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Missing protocol'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('Missing server port (http)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'http://localhost:');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('Missing server port (https)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'https://localhost:');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('Invalid format (port is string)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(\n          formField,\n          'https://localhost:abc',\n        );\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('Invalid format (multiple ports)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(\n          formField,\n          'https://localhost:80:90',\n        );\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('Invalid format (invalid host)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWebDAVLoginView());\n        Finder formField = find.ancestor(\n          of: find.text('Server'),\n          matching: find.byType(TextFormField),\n        );\n        await tester.enterText(formField, 'https://local host:80');\n        await tester.tap(find.text('Apply'));\n        await tester.pumpAndSettle();\n        expect(\n          find.descendant(\n            of: formField,\n            matching: find.text('Invalid format'),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/setting/controller/setting_controller_test.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingController;\nimport 'package:ntodotxt/setting/model/setting_model.dart' show Setting;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  // Init ffi loader if needed.\n  sqfliteFfiInit();\n\n  late DatabaseController controller;\n  late SettingRepository repository;\n\n  setUp(() async {\n    controller = DatabaseController(inMemoryDatabasePath);\n    repository = SettingRepository(SettingController(controller));\n    await (await controller.database).delete('settings'); // Clear\n  });\n\n  group('list()', () {\n    test('empty', () async {\n      expect(await repository.list(), isEmpty);\n    });\n    test('filled', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      await (await controller.database).insert('settings', model.toMap());\n      expect(await repository.list(), [model]);\n    });\n  });\n\n  group('get()', () {\n    test('empty', () async {\n      expect(await repository.get(key: 'key1'), null);\n    });\n    test('filled', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      await (await controller.database).insert('settings', model.toMap());\n      expect(await repository.get(key: 'key1'), model);\n    });\n  });\n\n  group('insert()', () {\n    test('empty', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      expect(await repository.insert(model) > 0, isTrue);\n    });\n    test('filled', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      await (await controller.database).insert('settings', model.toMap());\n      expect(await repository.insert(model), 0);\n    });\n  });\n\n  group('update()', () {\n    test('empty', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      expect(await repository.update(model), 0);\n    });\n    test('filled', () async {\n      Setting model1 = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      Setting model2 = model1.copyWith(value: 'value2');\n      await (await controller.database).insert('settings', model1.toMap());\n      expect(await repository.update(model2) > 0, isTrue);\n    });\n  });\n\n  group('updateOrInsert()', () {\n    test('empty', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      expect(await repository.updateOrInsert(model) > 0, isTrue);\n    });\n    test('filled', () async {\n      Setting model1 = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      Setting model2 = model1.copyWith(value: 'value2');\n      await (await controller.database).insert('settings', model1.toMap());\n      expect(await repository.updateOrInsert(model2) > 0, isTrue);\n    });\n  });\n\n  group('delete()', () {\n    test('empty', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      expect(await repository.delete(key: model.key), 0);\n    });\n    test('filled', () async {\n      Setting model = const Setting(\n        key: 'key1',\n        value: 'value1',\n      );\n      await (await controller.database).insert('settings', model.toMap());\n      expect(await repository.delete(key: model.key) > 0, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "test/setting/page/settings_page_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show Filter, ListFilter, ListGroup, ListOrder;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/login/state/login_cubit.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingController;\nimport 'package:ntodotxt/setting/page/settings_page.dart' show SettingsPage;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass SettingsPageBlocProvider extends StatelessWidget {\n  final DatabaseController dbController;\n  final Filter? filter;\n\n  const SettingsPageBlocProvider({\n    this.filter,\n    this.dbController = const DatabaseController(inMemoryDatabasePath),\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        BlocProvider<LoginCubit>(\n          create: (BuildContext context) => LoginCubit(),\n        ),\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) => SettingRepository(\n            SettingController(dbController),\n          ),\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n        BlocProvider<InteractionSettingsCubit>(\n          create: (BuildContext context) => InteractionSettingsCubit(\n            repository: context.read<SettingRepository>(),\n          ),\n        ),\n      ],\n      child: MultiBlocProvider(\n        providers: [\n          BlocProvider<FilterCubit>(\n            create: (BuildContext context) => FilterCubit(\n              settingRepository: context.read<SettingRepository>(),\n              filterRepository: context.read<FilterRepository>(),\n              filter: filter ?? const Filter(),\n            )..load(),\n          ),\n          BlocProvider<TodoFileCubit>(\n            create: (BuildContext context) => TodoFileCubit(\n              repository: context.read<SettingRepository>(),\n            )..load(),\n          ),\n        ],\n        child: Builder(\n          builder: (BuildContext context) {\n            return const MaterialApp(\n              home: SettingsPage(),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nvoid main() {\n  group('Display settings', () {\n    group('order', () {\n      testWidgets('default value', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default order' &&\n                (widget.subtitle as Text).data == ListOrder.ascending.name,\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('update by dialog', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default order' &&\n                (widget.subtitle as Text).data == ListOrder.ascending.name,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(find.byType(Dialog), findsOneWidget);\n        await tester.tap(\n          find.byKey(Key('${ListOrder.descending.name}DialogRadioButton')),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default order' &&\n                (widget.subtitle as Text).data == ListOrder.descending.name,\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('filter', () {\n      testWidgets('default value', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default filter' &&\n                (widget.subtitle as Text).data == ListFilter.all.name,\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('update by dialog', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default filter' &&\n                (widget.subtitle as Text).data == ListFilter.all.name,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(find.byType(Dialog), findsOneWidget);\n        await tester.tap(\n          find.byKey(Key('${ListFilter.completedOnly.name}DialogRadioButton')),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default filter' &&\n                (widget.subtitle as Text).data == ListFilter.completedOnly.name,\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('group by', () {\n      testWidgets('default value', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default grouping' &&\n                (widget.subtitle as Text).data == ListGroup.none.name,\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('update by dialog', (tester) async {\n        await tester.pumpWidget(const SettingsPageBlocProvider());\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default grouping' &&\n                (widget.subtitle as Text).data == ListGroup.none.name,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(find.byType(Dialog), findsOneWidget);\n        await tester.tap(\n          find.byKey(Key('${ListGroup.priority.name}DialogRadioButton')),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.byWidgetPredicate(\n            (Widget widget) =>\n                widget is ListTile &&\n                (widget.title as Text).data == 'Default grouping' &&\n                (widget.subtitle as Text).data == ListGroup.priority.name,\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/api/todo_list_api_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:file/memory.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/repository/todo_list_repository.dart';\n\nFile mockTodoListFile(List<String> rawTodoList) {\n  final MemoryFileSystem fs = MemoryFileSystem();\n  final File file = fs.file('todo.txt');\n  file.createSync();\n  file.writeAsStringSync(\n    rawTodoList.join(Platform.lineTerminator),\n    flush: true,\n  );\n\n  return file;\n}\n\nTodoListRepository mockLocalTodoListRepository(File todoFile) {\n  final LocalTodoListApi api = LocalTodoListApi.fromFile(localFile: todoFile);\n  final TodoListRepository repository = TodoListRepository(api);\n\n  return repository;\n}\n\nvoid main() {\n  late File todoFile;\n  late TodoListRepository repository;\n\n  setUp(() {\n    todoFile = mockTodoListFile([]);\n    repository = mockLocalTodoListRepository(todoFile);\n  });\n\n  group('LocalTodoListApi', () {\n    group('init()', () {\n      test('initial file with initial todo', () async {\n        const List<String> todoListStr = [\n          '2023-11-23 Code something',\n        ];\n\n        todoFile = mockTodoListFile(todoListStr);\n        repository = mockLocalTodoListRepository(todoFile);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder(\n            [\n              [for (var s in todoListStr) Todo.fromString(value: s)],\n            ],\n          ),\n        );\n      });\n      test('initial file with multiple initial todos', () async {\n        List<String> todoListStr = [\n          'x 2023-12-03 2023-12-02 TodoA',\n          '1970-01-01 TodoB due:1970-01-01',\n          '2023-12-02 TodoC due:2023-12-04',\n          '2023-12-02 TodoD due:2023-12-05',\n          '2023-11-11 TodoE',\n        ];\n\n        todoFile = mockTodoListFile(todoListStr);\n        repository = mockLocalTodoListRepository(todoFile);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder(\n            [\n              [for (var s in todoListStr) Todo.fromString(value: s)],\n            ],\n          ),\n        );\n      });\n    });\n\n    group('read and write', () {\n      test('readFromSource()', () async {\n        const List<String> todoListStr = [\n          '2023-11-23 Code something',\n        ];\n\n        expect(await todoFile.readAsLines(), []);\n\n        todoFile.writeAsStringSync(\n          todoListStr.join(Platform.lineTerminator),\n          flush: true,\n        );\n\n        await repository.readFromSource();\n        expect(await todoFile.readAsLines(), todoListStr);\n      });\n      test('writeToSource()', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        repository.saveTodo(todo);\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), [todo.toString()]);\n      });\n    });\n\n    group('existsTodo()', () {\n      test('existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        repository.saveTodo(todo);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), [todo.toString()]);\n\n        expect(repository.existsTodo(todo), true);\n      });\n      test('non-existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n\n        expect(await todoFile.readAsLines(), []);\n\n        expect(repository.existsTodo(todo), false);\n      });\n    });\n\n    group('saveTodo()', () {\n      test('create new todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        repository.saveTodo(todo);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), [todo.toString()]);\n      });\n      test('update existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something other',\n        );\n        repository.saveTodo(todo);\n        repository.saveTodo(todo2); // Update existing one.\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo2],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(\n          await todoFile.readAsLines(),\n          [todo2.toString()],\n        );\n      });\n      test('update/save non-existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '2',\n          value: '2023-11-23 Code something other',\n        );\n        repository.saveTodo(todo);\n        repository.saveTodo(todo2); // Update non-existing one.\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo, todo2],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(\n            await todoFile.readAsLines(), [todo.toString(), todo2.toString()]);\n      });\n    });\n\n    group('deleteTodo()', () {\n      test('delete existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        repository.saveTodo(todo);\n        repository.deleteTodo(todo);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), []);\n      });\n      test('delete non-existing todo', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '2',\n          value: '2023-11-23 Code something other',\n        );\n        repository.saveTodo(todo);\n        repository.deleteTodo(todo2); // Delete non-existing todo.\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), [todo.toString()]);\n      });\n    });\n\n    group('saveMultipleTodos()', () {\n      test('create and update todos', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '2',\n          value: '2023-11-23 Code something other',\n        );\n        repository.saveTodo(todo);\n        repository.saveTodo(todo2);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo, todo2],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(\n          await todoFile.readAsLines(),\n          [\n            todo.toString(),\n            todo2.toString(),\n          ],\n        );\n\n        final Todo todoUpdate = todo.copyWith(\n          completion: true,\n          completionDate: DateTime.now(),\n        );\n        final Todo todo2Update = todo2.copyWith(\n          completion: true,\n          completionDate: DateTime.now(),\n        );\n        repository.saveMultipleTodos([todoUpdate, todo2Update]);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder(\n            [\n              [todoUpdate, todo2Update],\n            ],\n          ),\n        );\n\n        await repository.writeToSource();\n        expect(\n          await todoFile.readAsLines(),\n          [\n            todoUpdate.toString(),\n            todo2Update.toString(),\n          ],\n        );\n      });\n    });\n\n    group('deleteMultipleTodos()', () {\n      test('delete todos', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '2',\n          value: '2023-11-23 Code something other',\n        );\n        repository.saveTodo(todo);\n        repository.saveTodo(todo2);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [todo, todo2],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(\n          await todoFile.readAsLines(),\n          [\n            todo.toString(),\n            todo2.toString(),\n          ],\n        );\n\n        repository.deleteMultipleTodos([todo, todo2]);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), []);\n      });\n      test('delete non-existing todos', () async {\n        final Todo todo = Todo.fromString(\n          id: '1',\n          value: '2023-11-23 Code something',\n        );\n        final Todo todo2 = Todo.fromString(\n          id: '2',\n          value: '2023-11-23 Code something other',\n        );\n\n        repository.deleteMultipleTodos([todo, todo2]);\n\n        await expectLater(\n          repository.getTodoList(),\n          emitsInOrder([\n            [],\n          ]),\n        );\n\n        await repository.writeToSource();\n        expect(await todoFile.readAsLines(), []);\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/model/todo_model_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\n\nvoid main() {\n  setUp(() {});\n\n  group('todo Todo()', () {\n    group('completion & completionDate', () {\n      test('initial incompleted', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.completion, false);\n        expect(todo.completionDate, null);\n      });\n      test('initial completed', () {\n        final DateTime now = DateTime.now();\n        final Todo todo = Todo(\n          completion: true,\n          description: 'Write some tests',\n        );\n        expect(todo.completion, true);\n        expect(todo.completionDate, DateTime(now.year, now.month, now.day));\n      });\n      test('initial completed & completionDate', () {\n        final Todo todo = Todo(\n          completion: true,\n          completionDate: DateTime(1970, 1, 1),\n          description: 'Write some tests',\n        );\n        expect(todo.completion, true);\n        expect(todo.completionDate, DateTime(1970, 1, 1));\n      });\n    });\n\n    group('priority', () {\n      test('no initial priority', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.priority, Priority.none);\n      });\n      test('with initial priority', () {\n        final Todo todo = Todo(\n          priority: Priority.A,\n          description: 'Write some tests',\n        );\n        expect(todo.priority, Priority.A);\n      });\n    });\n\n    group('creationDate', () {\n      test('no initial creationDate', () {\n        final DateTime now = DateTime.now();\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.creationDate, DateTime(now.year, now.month, now.day));\n      });\n      test('with initial creationDate', () {\n        final DateTime now = DateTime.now();\n        final Todo todo = Todo(\n          creationDate: now,\n          description: 'Write some tests',\n        );\n        expect(todo.creationDate, DateTime(now.year, now.month, now.day));\n      });\n    });\n\n    group('description', () {\n      test('no initial description', () {\n        final Todo todo = Todo();\n        expect(todo.description, '');\n      });\n      test('with initial description', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.description, 'Write some tests');\n      });\n    });\n\n    group('projects', () {\n      test('no initial projects', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.projects, []);\n      });\n      test('with initial projects', () {\n        final Todo todo = Todo(\n          description: 'Write some tests +project1',\n        );\n        expect(todo.projects, {'project1'});\n      });\n    });\n\n    group('contexts', () {\n      test('no initial contexts', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.contexts, []);\n      });\n      test('with initial contexts', () {\n        final Todo todo = Todo(\n          description: 'Write some tests @context1',\n        );\n        expect(todo.contexts, {'context1'});\n      });\n    });\n\n    group('keyValues', () {\n      test('no initial keyValues', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        expect(todo.keyValues, []);\n      });\n      test('with initial keyValues', () {\n        final Todo todo = Todo(\n          description: 'Write some tests key:value',\n        );\n        expect(todo.keyValues, {'key:value'});\n      });\n    });\n  });\n\n  group('todo copyWith()', () {\n    group('completion & completionDate', () {\n      test('set completion', () {\n        final DateTime now = DateTime.now();\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(completion: true);\n        expect(todo2.completion, true);\n        expect(todo2.completionDate, DateTime(now.year, now.month, now.day));\n      });\n      test('set completion & completionDate', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(\n            completion: true, completionDate: DateTime(1970, 1, 1));\n        expect(todo2.completion, true);\n        expect(todo2.completionDate, DateTime(1970, 1, 1));\n      });\n      test('unset completion', () {\n        final Todo todo = Todo(\n          completion: true,\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(completion: false);\n        expect(todo2.completion, false);\n        expect(todo2.completionDate, null);\n      });\n    });\n\n    group('priority', () {\n      test('set priority', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(priority: Priority.A);\n        expect(todo2.priority, Priority.A);\n      });\n      test('unset priority', () {\n        final Todo todo = Todo(\n          priority: Priority.A,\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(priority: Priority.none);\n        expect(todo2.priority, Priority.none);\n      });\n    });\n\n    group('creationDate', () {});\n\n    group('description', () {\n      test('set description', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(description: 'Write more tests');\n        expect(todo2.description, 'Write more tests');\n      });\n      test('unset description', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(description: '');\n        expect(todo2.description, '');\n      });\n    });\n\n    group('projects', () {\n      test('set projects', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests +project2');\n        expect(todo2.projects, {'project2'});\n      });\n      test('unset projects', () {\n        final Todo todo = Todo(\n          description: 'Write some tests +project2',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests');\n        expect(todo2.projects, []);\n      });\n    });\n\n    group('contexts', () {\n      test('set contexts', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests @context2');\n        expect(todo2.contexts, {'context2'});\n      });\n      test('unset contexts', () {\n        final Todo todo = Todo(\n          description: 'Write some tests @context2',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests');\n        expect(todo2.contexts, []);\n      });\n    });\n\n    group('keyValues', () {\n      test('set keyValues', () {\n        final Todo todo = Todo(\n          description: 'Write some tests',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests key:value');\n        expect(todo2.keyValues, {'key:value'});\n      });\n      test('unset keyValues', () {\n        final Todo todo = Todo(\n          description: 'Write some tests key:value',\n        );\n        final todo2 = todo.copyWith(description: 'Write some tests');\n        expect(todo2.keyValues, []);\n      });\n    });\n  });\n\n  group('todo copyDiff()', () {\n    test('copy explizit set attributes but keep creationDate if set', () {\n      final DateTime now = DateTime.now();\n      final Todo todo = Todo(\n        priority: Priority.A,\n        description: 'Write some tests',\n      );\n      final Todo todo2 = todo.copyDiff(completion: true);\n      expect(todo2.completion, true);\n      expect(todo2.completionDate, DateTime(now.year, now.month, now.day));\n      expect(todo2.creationDate, DateTime(now.year, now.month, now.day));\n      expect(todo2.priority, Priority.none);\n      expect(todo2.description, '');\n    });\n  });\n\n  group('todo copyMerge()', () {\n    test('do not overwrite attrs if not set in the diff', () {\n      final DateTime now = DateTime.now();\n      Todo todo = Todo(\n        completion: false,\n        priority: Priority.A,\n        creationDate: now,\n        description: 'Write some tests +project1 @context1 key:value',\n      );\n      final Todo diff = todo.copyDiff(completion: true);\n      todo = todo.copyWith(\n        priority: Priority.B,\n        description: 'Write more tests +project1 @context1 key:value',\n      );\n      final Todo todo2 = diff.copyMerge(todo);\n      expect(todo2.priority, Priority.B);\n      expect(\n          todo2.description, 'Write more tests +project1 @context1 key:value');\n      expect(todo2.projects, {'project1'});\n      expect(todo2.contexts, {'context1'});\n      expect(todo2.keyValues, {'key:value'});\n      expect(todo2.completion, true);\n      expect(todo2.completionDate, DateTime(now.year, now.month, now.day));\n    });\n  });\n\n  group('todo fromString()', () {\n    group('todo completion', () {\n      group('completed', () {\n        test('short todo (RangeError)', () {\n          final Todo todo = Todo.fromString(value: 'x 2022-11-16 Todo');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(2022, 11, 16));\n        });\n        test('simple todo', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-08-22 Write some tests');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(2022, 08, 22));\n        });\n\n        test('full todo', () {\n          final Todo todo = Todo.fromString(\n              value:\n                  'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(2022, 11, 16));\n        });\n      });\n      group('incompleted', () {\n        test('short todo (RangeError)', () {\n          final Todo todo = Todo.fromString(value: 'Todo');\n          expect(todo.completion, false);\n        });\n        test('simple todo', () {\n          final Todo todo = Todo.fromString(value: 'Write some tests');\n          expect(todo.completion, false);\n        });\n        test('missing whitespace', () {\n          final Todo todo = Todo.fromString(value: 'xWrite some tests');\n          expect(todo.completion, false);\n        });\n        test('wrong mark', () {\n          final Todo todo = Todo.fromString(value: 'X Write some tests');\n          expect(todo.completion, false);\n        });\n        test('wrong position', () {\n          final Todo todo = Todo.fromString(value: '(A) x Write some tests');\n          expect(todo.completion, false);\n        });\n      });\n      group('edge cases', () {\n        test('missing completion date', () {\n          final DateTime now = DateTime.now();\n          final Todo todo = Todo.fromString(value: 'x Write some tests');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(now.year, now.month, now.day));\n        });\n      });\n    });\n\n    group('todo priority', () {\n      group('with priority', () {\n        test('incompleted short todo (RangeError)', () {\n          final todo = Todo.fromString(value: '(A) Todo');\n          expect(todo.priority, Priority.A);\n        });\n        test('incompleted simple todo (RangeError)', () {\n          final todo = Todo.fromString(value: '(A) Write some tests');\n          expect(todo.priority, Priority.A);\n        });\n        test('incompleted full todo', () {\n          final todo = Todo.fromString(\n            value:\n                '(A) 2022-11-16 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.priority, Priority.A);\n        });\n        test('incompleted todo with very low priority', () {\n          final todo = Todo.fromString(value: '(Z) Todo');\n          expect(todo.priority, Priority.Z);\n        });\n        test('completed short todo (RangeError)', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 (A) Todo');\n          expect(todo.priority, Priority.A);\n        });\n        test('completed simple todo', () {\n          final todo =\n              Todo.fromString(value: 'x 2022-11-16 (A) Write some tests');\n          expect(todo.priority, Priority.A);\n        });\n        test('completed full todo', () {\n          final todo = Todo.fromString(\n            value:\n                'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.priority, Priority.A);\n        });\n        test('completed todo with very low priority', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 (Z) Todo');\n          expect(todo.priority, Priority.Z);\n        });\n      });\n      group('without priority', () {\n        test('incompleted short todo (RangeError)', () {\n          final todo = Todo.fromString(value: 'Todo');\n          expect(todo.priority, Priority.none);\n        });\n        test('incompleted simple todo', () {\n          final todo = Todo.fromString(value: 'Write some tests');\n          expect(todo.priority, Priority.none);\n        });\n        test('incompleted full todo', () {\n          final todo = Todo.fromString(\n              value:\n                  '2022-11-16 Write some tests +project @context due:2022-12-31');\n          expect(todo.priority, Priority.none);\n        });\n        test('completed short todo (RangeError)', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 Todo');\n          expect(todo.priority, Priority.none);\n        });\n        test('completed simple todo', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests');\n          expect(todo.priority, Priority.none);\n        });\n        test('completed full todo', () {\n          final todo = Todo.fromString(\n            value:\n                'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.priority, Priority.none);\n        });\n        test('missing parenthesis', () {\n          final todo = Todo.fromString(value: 'A Write some tests');\n          expect(todo.priority, Priority.none);\n        });\n        test('missing whitespace', () {\n          final todo = Todo.fromString(value: '(A)Write some tests');\n          expect(todo.priority, Priority.none);\n        });\n        test('wrong priority sign', () {\n          final todo = Todo.fromString(value: '(a) Write some tests');\n          expect(todo.priority, Priority.none);\n        });\n        test('wrong position', () {\n          final todo = Todo.fromString(value: 'Write some tests (A)');\n          expect(todo.priority, Priority.none);\n        });\n      });\n    });\n\n    group('todo creation date', () {\n      group('with creation date', () {\n        test('incompleted simple todo', () {\n          final todo = Todo.fromString(value: '2022-11-01 Write some tests');\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n        test('incompleted and with priority simple todo', () {\n          final todo =\n              Todo.fromString(value: '(A) 2022-11-01 Write some tests');\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n        test('incompleted full todo', () {\n          final todo = Todo.fromString(\n            value:\n                '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n        test('completed simple todo', () {\n          final todo = Todo.fromString(\n              value: 'x 2022-11-16 2022-11-01 Write some tests');\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n        test('completed and with priority simple todo', () {\n          final todo = Todo.fromString(\n              value: 'x 2022-11-16 (A) 2022-11-01 Write some tests');\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n        test('completed full todo', () {\n          final todo = Todo.fromString(\n            value:\n                'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.creationDate, DateTime.parse('2022-11-01'));\n        });\n      });\n    });\n\n    group('todo completion date', () {\n      group('with completion date', () {\n        test('completed simple todo', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests');\n          expect(todo.completionDate, DateTime.parse('2022-11-16'));\n        });\n        test('completed and with priority simple todo', () {\n          final todo =\n              Todo.fromString(value: 'x 2022-11-16 (A) Write some tests');\n          expect(todo.completionDate, DateTime.parse('2022-11-16'));\n        });\n        test('completed full todo', () {\n          final todo = Todo.fromString(\n            value:\n                'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.completionDate, DateTime.parse('2022-11-16'));\n        });\n      });\n      group('without completion date', () {\n        test('incompleted simple todo', () {\n          final todo = Todo.fromString(value: 'Write some tests');\n          expect(todo.completionDate, null);\n        });\n        test('incompleted and with priority simple todo', () {\n          final todo = Todo.fromString(value: '(A) Write some tests');\n          expect(todo.completionDate, null);\n        });\n        test('incompleted full todo', () {\n          final todo = Todo.fromString(\n            value:\n                '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.completionDate, null);\n        });\n      });\n      group('edge cases', () {\n        test(\n            'incompleted with forbidden completiond date (is recognized as part of description)',\n            () {\n          final todo = Todo.fromString(\n            value: '2022-11-16 2022-11-01 Write some tests',\n          );\n          expect(todo.completionDate, null);\n          expect(todo.creationDate, DateTime.parse('2022-11-16'));\n          expect(todo.description, '2022-11-01 Write some tests');\n        });\n        test(\n            'incompleted with priority and forbidden completion date (is recognized as part of description)',\n            () {\n          final todo = Todo.fromString(\n            value: '(A) 2022-11-16 2022-11-01 Write some tests',\n          );\n          expect(todo.priority, Priority.A);\n          expect(todo.completionDate, null);\n          expect(todo.creationDate, DateTime.parse('2022-11-16'));\n          expect(todo.description, '2022-11-01 Write some tests');\n        });\n        test('completed and missing completion date', () {\n          final DateTime now = DateTime.now();\n          final Todo todo = Todo.fromString(value: 'x Write some tests');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(now.year, now.month, now.day));\n        });\n        test('completed with priority and missing completion date', () {\n          final DateTime now = DateTime.now();\n          final Todo todo = Todo.fromString(value: 'x (A) Write some tests');\n          expect(todo.completion, true);\n          expect(todo.completionDate, DateTime(now.year, now.month, now.day));\n        });\n      });\n    });\n\n    group('todo projects', () {\n      test('no project tags', () {\n        final todo = Todo.fromString(value: 'Write some tests');\n        expect(todo.projects, []);\n      });\n      test('single project tag', () {\n        final todo = Todo.fromString(value: 'Write some tests +project');\n        expect(todo.projects, ['project']);\n      });\n      test('multiple project tags', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests +project1 +project2');\n        expect(todo.projects, ['project1', 'project2']);\n      });\n      test('multiple project tags (not in sequence)', () {\n        final todo = Todo.fromString(value: 'Write some +tests for +project');\n        expect(todo.projects, ['project', 'tests']);\n      });\n      test('project tag with a special name', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests +project_123+a-b-c');\n        expect(todo.projects, ['project_123+a-b-c']);\n      });\n      test('project tag with uppercase characters', () {\n        final todo = Todo.fromString(value: 'Write some tests +Project');\n        expect(todo.projects, ['Project']);\n      });\n      test('project tag with project duplication', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests +project +project');\n        expect(todo.projects, ['project']);\n      });\n      test('similar project tags with uppercase and lowercase characters', () {\n        final todo = Todo.fromString(\n            value: 'Write some tests +project +project +Project');\n        expect(todo.projects, ['Project', 'project']);\n      });\n      test('incompleted full todo', () {\n        final todo = Todo.fromString(\n          value: '2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.projects, ['project']);\n      });\n      test('incompleted with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.projects, ['project']);\n      });\n      test('completed full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.projects, ['project']);\n      });\n      test('completed with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.projects, ['project']);\n      });\n    });\n\n    group('todo contexts', () {\n      test('no context tag', () {\n        final todo = Todo.fromString(value: 'Write some tests');\n        expect(todo.contexts, []);\n      });\n      test('single context tag', () {\n        final todo = Todo.fromString(value: 'Write some @context');\n        expect(todo.contexts, ['context']);\n      });\n      test('multiple context tags', () {\n        final todo = Todo.fromString(value: 'Write some @context1 @context2');\n        expect(todo.contexts, ['context1', 'context2']);\n      });\n      test('multiple context tags (not in sequence)', () {\n        final todo = Todo.fromString(value: 'Write some @tests for @context');\n        expect(todo.contexts, ['context', 'tests']);\n      });\n      test('context tag with a special name', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests for @context_123+a-b-c');\n        expect(todo.contexts, ['context_123+a-b-c']);\n      });\n      test('context tag with uppercase characters', () {\n        final todo = Todo.fromString(value: 'Write some tests @Context');\n        expect(todo.contexts, ['Context']);\n      });\n      test('context tag with context duplication', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests @context @context');\n        expect(todo.contexts, ['context']);\n      });\n      test('similar context tags with uppercase and lowercase characters', () {\n        final todo = Todo.fromString(\n            value: 'Write some tests +project @context @Context');\n        expect(todo.contexts, ['Context', 'context']);\n      });\n      test('incompleted full todo', () {\n        final todo = Todo.fromString(\n          value: '2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.contexts, ['context']);\n      });\n      test('incompleted with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.contexts, ['context']);\n      });\n      test('completed full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.contexts, ['context']);\n      });\n      test('completed with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.contexts, ['context']);\n      });\n    });\n\n    group('todo key values', () {\n      test('no key value tag', () {\n        final todo = Todo.fromString(value: 'Write some tests');\n        expect(todo.keyValues, []);\n      });\n      test('single key value tag', () {\n        final todo = Todo.fromString(value: 'Write some tests key:value');\n        expect(todo.keyValues, {'key:value'});\n      });\n      test('single character key value tag', () {\n        final todo = Todo.fromString(value: 'Write some tests k:v');\n        expect(todo.keyValues, {'k:v'});\n      });\n      test('multiple key value tags', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests key1:value1 key2:value2');\n        expect(todo.keyValues, {'key1:value1', 'key2:value2'});\n      });\n      test('multiple key value tags (not in sequence)', () {\n        final todo =\n            Todo.fromString(value: 'Write some key1:value1 tests key2:value2');\n        expect(todo.keyValues, {'key1:value1', 'key2:value2'});\n      });\n      test('key value tag with a special name', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests key-123:value_123');\n        expect(todo.keyValues, {'key-123:value_123'});\n      });\n      test('key value tag with uppercase characters', () {\n        final todo = Todo.fromString(value: 'Write some tests Key:Value');\n        expect(todo.keyValues, {'Key:Value'});\n      });\n      test('key value tag with key value duplication', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests key:value key:value');\n        expect(todo.keyValues, {'key:value'});\n      });\n      test('similar key value tags with uppercase and lowercase characters',\n          () {\n        final todo =\n            Todo.fromString(value: 'Write some tests key:value Key:Value');\n        expect(todo.keyValues, {'Key:Value', 'key:value'});\n      });\n      test('invalid key value tag 1', () {\n        final todo =\n            Todo.fromString(value: 'Write some tests key1:value1:invalid');\n        expect(todo.keyValues, []);\n      });\n      test('invalid key value tag 2', () {\n        final todo = Todo.fromString(value: 'Write some tests key1::invalid');\n        expect(todo.keyValues, []);\n      });\n      test('incompleted full todo', () {\n        final todo = Todo.fromString(\n          value: '2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.keyValues, {'due:2022-12-31'});\n      });\n      test('incompleted with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.keyValues, {'due:2022-12-31'});\n      });\n      test('completed full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.keyValues, {'due:2022-12-31'});\n      });\n      test('completed with priority full todo', () {\n        final todo = Todo.fromString(\n          value:\n              'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n        );\n        expect(todo.keyValues, {'due:2022-12-31'});\n      });\n    });\n\n    group('todo description', () {\n      group('with description', () {\n        test('incompleted with description', () {\n          final todo = Todo.fromString(value: 'Write some tests');\n          expect(todo.description, 'Write some tests');\n        });\n        test('incompleted with description and priority', () {\n          final todo = Todo.fromString(value: '(A) Write some tests');\n          expect(todo.description, 'Write some tests');\n        });\n        test('incompleted full todo', () {\n          final todo = Todo.fromString(\n            value:\n                '(A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.description,\n              'Write some tests +project @context due:2022-12-31');\n        });\n        test('completed with description', () {\n          final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests');\n          expect(todo.description, 'Write some tests');\n        });\n        test('completed with description and priority', () {\n          final todo = Todo.fromString(\n            value: 'x 2022-11-16 (A) Write some tests',\n          );\n          expect(todo.description, 'Write some tests');\n        });\n        test('completed full todo', () {\n          final todo = Todo.fromString(\n            value:\n                'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31',\n          );\n          expect(todo.description,\n              'Write some tests +project @context due:2022-12-31');\n        });\n      });\n\n      group('empty description', () {\n        test('completed with projects', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-11-16 +project1 +project2');\n          expect(todo.description, '+project1 +project2');\n        });\n        test('completed with contexts', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-11-16 @context1 @context2');\n          expect(todo.description, '@context1 @context2');\n        });\n        test('completed with key-values', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-11-16 key1:val1 key2:val2');\n          expect(todo.description, 'key1:val1 key2:val2');\n        });\n        test('completed with all kind of tags', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-11-16 +project @context key:val');\n          expect(todo.description, '+project @context key:val');\n        });\n        test('completed', () {\n          final Todo todo = Todo.fromString(value: 'x 2022-11-16');\n          expect(todo.description, '');\n        });\n        test('completed with priority', () {\n          final Todo todo = Todo.fromString(value: 'x 2022-11-16 (A)');\n          expect(todo.description, '');\n        });\n        test('completed with priority and creation date', () {\n          final Todo todo =\n              Todo.fromString(value: 'x 2022-11-16 (A) 2022-11-01');\n          expect(todo.description, '');\n        });\n        test('incompleted with projects', () {\n          final Todo todo = Todo.fromString(value: '+project1 +project2');\n          expect(todo.description, '+project1 +project2');\n        });\n        test('incompleted with contexts', () {\n          final Todo todo = Todo.fromString(value: '@context1 @context2');\n          expect(todo.description, '@context1 @context2');\n        });\n        test('incompleted with key-values', () {\n          final Todo todo = Todo.fromString(value: 'key1:val1 key2:val2');\n          expect(todo.description, 'key1:val1 key2:val2');\n        });\n        test('incompleted with all kind of tags', () {\n          final Todo todo = Todo.fromString(value: '+project @context key:val');\n          expect(todo.description, '+project @context key:val');\n        });\n        test('incompleted', () {\n          final Todo todo = Todo.fromString(value: '');\n          expect(todo.description, '');\n        });\n        test('incompleted with priority', () {\n          final Todo todo = Todo.fromString(value: '(A)');\n          expect(todo.description, '');\n        });\n        test('incompleted with priority and creation date', () {\n          final Todo todo = Todo.fromString(value: '(A) 2022-11-01');\n          expect(todo.description, '');\n        });\n      });\n    });\n  });\n\n  group('todo dueDate', () {\n    test('unset', () {\n      final todo = Todo.fromString(\n        value: '2022-11-01 Write some tests',\n      );\n      expect(todo.dueDate, null);\n    });\n    test('set', () {\n      final todo = Todo.fromString(\n        value: '2022-11-01 Write some tests due:2023-12-31',\n      );\n      expect(todo.dueDate, DateTime(2023, 12, 31));\n    });\n    test('set but invalid', () {\n      final todo = Todo.fromString(\n        value: '2022-11-01 Write some tests due:yyyy-mm-dd',\n      );\n      expect(todo.dueDate, null);\n    });\n  });\n\n  group('todo toString()', () {\n    test('full todo', () {\n      const String value =\n          'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31';\n      final todo = Todo.fromString(\n        value: value,\n      );\n      expect(todo.toString(), value);\n    });\n    test('full todo with multiple whitespace', () {\n      final todo = Todo.fromString(\n        value:\n            'x  2022-11-16  (A)  2022-11-01  Write some tests +project @context due:2022-12-31',\n      );\n      expect(todo.toString(),\n          'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31');\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/page/todo_create_edit_page_test.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/common/widget/chip.dart';\nimport 'package:ntodotxt/common/widget/contexts_dialog.dart';\nimport 'package:ntodotxt/common/widget/key_values_dialog.dart';\nimport 'package:ntodotxt/common/widget/priorities_dialog.dart';\nimport 'package:ntodotxt/common/widget/projects_dialog.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/page/todo_create_edit_page.dart';\n\nclass MaterialAppWrapper extends StatelessWidget {\n  final Todo? initTodo;\n  final Set<String> projects;\n  final Set<String> contexts;\n  final Set<String> keyValues;\n\n  const MaterialAppWrapper({\n    this.initTodo,\n    this.projects = const {},\n    this.contexts = const {},\n    this.keyValues = const {},\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: TodoCreateEditPage(\n        initTodo: initTodo ?? Todo(),\n        newTodo: initTodo == null ? true : false,\n        projects: projects,\n        contexts: contexts,\n        keyValues: keyValues,\n      ),\n    );\n  }\n}\n\nvoid main() {\n  group('TodoCreateEditPage', () {\n    group('narrow view', () {\n      group('create mode', () {\n        testWidgets('found no SaveTodoIconButton if name is empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n        });\n        testWidgets('found SaveTodoIconButton if name is not empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          await tester.enterText(find.byType(TextFormField), 'Filter name');\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n        });\n        testWidgets('found no DeleteTodoIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteTodoIconButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no TodoCompletionDateItem', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(TodoCompletionDateItem),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no DoneUndonePrimaryButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DoneUndonePrimaryButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n      group('edit mode', () {\n        testWidgets('found no SaveTodoIconButton if todo has not be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveTodoIconButton if todo has be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo: Todo(description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          await tester.dragUntilVisible(\n            find.byType(TodoPriorityItem),\n            find.byType(ListView),\n            const Offset(0, -100),\n          );\n          await tester.tap(find.byType(TodoPriorityItem));\n          await tester.pumpAndSettle();\n          await tester.ensureVisible(find.byType(TodoPriorityTagDialog));\n          await tester.pumpAndSettle();\n          await tester.tap(\n            find.descendant(\n              of: find.byType(TodoPriorityTagDialog),\n              matching: find.text('A'),\n            ),\n          );\n          await tester.drag(find.byType(DraggableScrollableSheet),\n              const Offset(0, 500)); // Dismiss dialog.\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(TodoPriorityItem),\n              matching: find.text('A'),\n            ),\n            findsOneWidget,\n          );\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DeleteTodoIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteTodoIconButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found TodoCompletionDateItem', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(TodoCompletionDateItem),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DoneUndonePrimaryButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(400, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DoneUndonePrimaryButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n    });\n\n    group('wide view', () {\n      group('create mode', () {\n        testWidgets('found no SaveTodoIconButton if name is empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n        });\n        testWidgets('found SaveTodoIconButton if name is not empty',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          await tester.enterText(find.byType(TextFormField), 'Filter name');\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n        });\n        testWidgets('found no DeleteTodoIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteTodoIconButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no TodoCompletionDateItem', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(TodoCompletionDateItem),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found no DoneUndonePrimaryButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(const MaterialAppWrapper());\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DoneUndonePrimaryButton),\n            findsNothing,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n      group('edit mode', () {\n        testWidgets('found no SaveTodoIconButton if todo has not be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsNothing,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found SaveTodoIconButton if todo has be changed',\n            (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo: Todo(description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          await tester.dragUntilVisible(\n            find.byType(TodoPriorityItem),\n            find.byType(ListView),\n            const Offset(0, -100),\n          );\n          await tester.tap(find.byType(TodoPriorityItem));\n          await tester.pumpAndSettle();\n          await tester.ensureVisible(find.byType(TodoPriorityTagDialog));\n          await tester.pumpAndSettle();\n          await tester.tap(\n            find.descendant(\n              of: find.byType(TodoPriorityTagDialog),\n              matching: find.text('A'),\n            ),\n          );\n          await tester.drag(find.byType(DraggableScrollableSheet),\n              const Offset(0, 500)); // Dismiss dialog.\n          await tester.pumpAndSettle();\n          expect(\n            find.descendant(\n              of: find.byType(TodoPriorityItem),\n              matching: find.text('A'),\n            ),\n            findsOneWidget,\n          );\n          expect(\n            find.descendant(\n              of: find.byType(SaveTodoIconButton),\n              matching: find.byType(IconButton),\n            ),\n            findsOneWidget,\n          );\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DeleteTodoIconButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DeleteTodoIconButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found TodoCompletionDateItem', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(TodoCompletionDateItem),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n        testWidgets('found DoneUndonePrimaryButton', (tester) async {\n          // Increase size to ensure all elements in list are visible.\n          tester.view.physicalSize = const Size(800, 800);\n          tester.view.devicePixelRatio = 1.0;\n\n          await tester.pumpWidget(\n            MaterialAppWrapper(\n              initTodo:\n                  Todo(priority: Priority.A, description: 'Code something'),\n            ),\n          );\n          await tester.pumpAndSettle();\n          expect(\n            find.byType(DoneUndonePrimaryButton),\n            findsOneWidget,\n          );\n\n          // resets the screen to its original size after the test end\n          addTearDown(tester.view.resetPhysicalSize);\n          addTearDown(tester.view.resetDevicePixelRatio);\n        });\n      });\n    });\n\n    group('default values', () {\n      testWidgets('TodoPriorityItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoPriorityItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoPriorityItem),\n            matching: find.text('none'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoProjectTagsItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoContextTagsItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoKeyValueTagsItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCreationDateItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCreationDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoCreationDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDueDateItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('non default values', () {\n      testWidgets('TodoPriorityItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(priority: Priority.A, description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoPriorityItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoPriorityItem),\n            matching: find.text(Priority.A.name),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoProjectTagsItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something +project1 +project2'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project1',\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project2',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoContextTagsItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something @context1 @context2'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context1',\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context2',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoKeyValueTagsItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something key1:val1 key2:val2'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'key1:val1',\n            ),\n          ),\n          findsOneWidget,\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'key2:val2',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCompletionDateItem (incompleted)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCompletionDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoCompletionDateItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCompletionDateItem (completed)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(completion: true, description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCompletionDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoCompletionDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCreationDateItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCreationDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoCreationDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDueDateItem', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something due:2024-02-27'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text('2024-02-27'),\n          ),\n          findsOneWidget,\n        );\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'due:2024-02-27',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n\n    group('update values', () {\n      testWidgets('TodoDescriptionTextField (project)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDescriptionTextField),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoDescriptionTextField),\n            matching: find.byType(TextFormField),\n          ),\n          'Code something +project1',\n        );\n        await tester.pumpAndSettle();\n\n        await tester.dragUntilVisible(\n          find.byType(TodoProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDescriptionTextField (context)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDescriptionTextField),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoDescriptionTextField),\n            matching: find.byType(TextFormField),\n          ),\n          'Code something @context1',\n        );\n        await tester.pumpAndSettle();\n\n        await tester.dragUntilVisible(\n          find.byType(TodoContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDescriptionTextField (key value)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDescriptionTextField),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoDescriptionTextField),\n            matching: find.byType(TextFormField),\n          ),\n          'Code something key1:val1',\n        );\n        await tester.pumpAndSettle();\n\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'key1:val1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDescriptionTextField (due date)', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDescriptionTextField),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoDescriptionTextField),\n            matching: find.byType(TextFormField),\n          ),\n          'Code something due:2024-02-27',\n        );\n        await tester.pumpAndSettle();\n\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text('2024-02-27'),\n          ),\n          findsOneWidget,\n        );\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'due:2024-02-27',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoPriorityItem', (tester) async {\n        await tester.pumpWidget(const MaterialAppWrapper());\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoPriorityItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoPriorityItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoPriorityTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoPriorityTagDialog),\n            matching: find.text('A'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoPriorityItem),\n            matching: find.text('A'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoProjectTagsItem by tap chip', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            projects: {'project1', 'project2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoProjectTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoProjectTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoProjectTagDialog),\n            matching: find.text('project1'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoProjectTagsItem by enter in textfield', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            projects: {'project1', 'project2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoProjectTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoProjectTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoProjectTagDialog));\n        await tester.pumpAndSettle();\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoProjectTagDialog),\n            matching: find.byType(TextFormField),\n          ),\n          'project3',\n        );\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoProjectTagDialog),\n            matching: find.text('Add'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoProjectTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'project3',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoContextTagsItem by tap chip', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            contexts: {'context1', 'context2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoContextTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoContextTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoContextTagDialog),\n            matching: find.text('context1'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoContextTagsItem by enter in textfield', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            contexts: {'context1', 'context2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoContextTagsItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoContextTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoContextTagDialog));\n        await tester.pumpAndSettle();\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoContextTagDialog),\n            matching: find.byType(TextFormField),\n          ),\n          'context3',\n        );\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoContextTagDialog),\n            matching: find.text('Add'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoContextTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'context3',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoKeyValueTagsItem by tap chip', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            keyValues: {'key1:val1', 'key2:val2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -200),\n        );\n\n        await tester.tap(find.byType(TodoKeyValueTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoKeyValueTagDialog));\n        await tester.pumpAndSettle();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagDialog),\n            matching: find.text('key1:val1'),\n          ),\n        );\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'key1:val1',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoKeyValueTagsItem by enter in textfield', (tester) async {\n        await tester.pumpWidget(\n          const MaterialAppWrapper(\n            keyValues: {'key1:val1', 'key2:val2'},\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoKeyValueTagsItem),\n          find.byType(ListView),\n          const Offset(0, -200),\n        );\n\n        await tester.tap(find.byType(TodoKeyValueTagsItem));\n        await tester.pumpAndSettle();\n\n        await tester.ensureVisible(find.byType(TodoKeyValueTagDialog));\n        await tester.pumpAndSettle();\n        await tester.enterText(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagDialog),\n            matching: find.byType(TextFormField),\n          ),\n          'key3:val3',\n        );\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagDialog),\n            matching: find.text('Add'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.drag(find.byType(DraggableScrollableSheet),\n            const Offset(0, 500)); // Dismiss dialog.\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoKeyValueTagsItem),\n            matching: find.byWidgetPredicate(\n              (Widget widget) =>\n                  widget is BasicChip && widget.label == 'key3:val3',\n            ),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCompletionDateItem (set)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(\n              completion: false,\n              description: 'Code something',\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCompletionDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(DoneUndonePrimaryButton));\n        await tester.pumpAndSettle();\n\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoCompletionDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCompletionDateItem (already set)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'x 2024-01-01 Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCompletionDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoCompletionDateItem));\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.descendant(\n            of: find.byType(DatePickerDialog),\n            matching: find.text('OK'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoCompletionDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoCompletionDateItem (unset)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(completion: true, description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoCompletionDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(DoneUndonePrimaryButton));\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoCompletionDateItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDueDateItem (set)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoDueDateItem));\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.descendant(\n            of: find.byType(DatePickerDialog),\n            matching: find.text('OK'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDueDateItem (already set)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something due:2024-02-27'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(find.byType(TodoDueDateItem));\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.descendant(\n            of: find.byType(DatePickerDialog),\n            matching: find.text('OK'),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        final DateTime initial = DateTime.parse('2024-02-27');\n        final String today =\n            '${initial.year.toString()}-${initial.month.toString().padLeft(2, '0')}-${initial.day.toString().padLeft(2, '0')}';\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text(today),\n          ),\n          findsOneWidget,\n        );\n      });\n      testWidgets('TodoDueDateItem (unset)', (tester) async {\n        await tester.pumpWidget(\n          MaterialAppWrapper(\n            initTodo: Todo(description: 'Code something due:2024-02-27'),\n          ),\n        );\n        await tester.pumpAndSettle();\n        await tester.dragUntilVisible(\n          find.byType(TodoDueDateItem),\n          find.byType(ListView),\n          const Offset(0, -100),\n        );\n\n        await tester.tap(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.byType(IconButton),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.descendant(\n            of: find.byType(TodoDueDateItem),\n            matching: find.text('-'),\n          ),\n          findsOneWidget,\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/page/todo_list_page_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:file/memory.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/database/controller/database.dart';\nimport 'package:ntodotxt/filter/controller/filter_controller.dart';\nimport 'package:ntodotxt/filter/model/filter_model.dart'\n    show Filter, ListFilter, ListGroup, ListOrder;\nimport 'package:ntodotxt/filter/repository/filter_repository.dart';\nimport 'package:ntodotxt/filter/state/filter_cubit.dart';\nimport 'package:ntodotxt/filter/state/filter_list_bloc.dart';\nimport 'package:ntodotxt/filter/state/filter_list_event.dart';\nimport 'package:ntodotxt/setting/controller/setting_controller.dart'\n    show SettingController;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/setting/state/interaction_settings_cubit.dart';\nimport 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/todo/page/todo_list_page.dart';\nimport 'package:ntodotxt/todo/repository/todo_list_repository.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:sqflite_common_ffi/sqflite_ffi.dart';\n\nclass TodoListPageMaterialApp extends StatelessWidget {\n  final DatabaseController dbController =\n      const DatabaseController(inMemoryDatabasePath);\n  final File localFile;\n  final Filter? filter;\n\n  const TodoListPageMaterialApp({\n    required this.localFile,\n    this.filter,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiRepositoryProvider(\n      providers: [\n        RepositoryProvider<SettingRepository>(\n          create: (BuildContext context) =>\n              SettingRepository(SettingController(dbController)),\n        ),\n        RepositoryProvider<TodoListRepository>(\n          create: (BuildContext context) {\n            return TodoListRepository(\n                LocalTodoListApi.fromFile(localFile: localFile));\n          },\n        ),\n        RepositoryProvider<FilterRepository>(\n          create: (BuildContext context) => FilterRepository(\n            FilterController(dbController),\n          ),\n        ),\n        BlocProvider<InteractionSettingsCubit>(\n          create: (BuildContext context) => InteractionSettingsCubit(\n            repository: context.read<SettingRepository>(),\n          ),\n        ),\n      ],\n      child: Builder(\n        builder: (BuildContext context) {\n          return MultiBlocProvider(\n            providers: [\n              BlocProvider<FilterCubit>(\n                create: (BuildContext context) => FilterCubit(\n                  settingRepository: context.read<SettingRepository>(),\n                  filterRepository: context.read<FilterRepository>(),\n                  filter: filter ?? const Filter(),\n                ),\n              ),\n              BlocProvider(\n                create: (BuildContext context) => TodoListBloc(\n                  repository: context.read<TodoListRepository>(),\n                )\n                  ..add(const TodoListSubscriptionRequested())\n                  ..add(const TodoListSynchronizationRequested()),\n              ),\n              BlocProvider<FilterListBloc>(\n                create: (BuildContext context) {\n                  return FilterListBloc(\n                    repository: context.read<FilterRepository>(),\n                  )..add(const FilterListSubscriped());\n                },\n              ),\n            ],\n            child: MaterialApp(\n              home: TodoListPage(filter: filter ?? const Filter()),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nvoid main() {\n  final MemoryFileSystem fs = MemoryFileSystem();\n  late File file;\n\n  setUp(() async {});\n\n  group('Order', () {\n    setUp(() async {\n      file = fs.file('todoOrder.txt');\n      await file.create();\n      await file.writeAsString(\n        [\n          '2023-12-02 TODOC',\n          '2023-12-02 todoA',\n          '2023-12-02 TodoB',\n        ].join('\\n'),\n        flush: true,\n      );\n    });\n    testWidgets('default', (tester) async {\n      final List<String> expectedTiles = [\n        'todoA',\n        'TodoB',\n        'TODOC',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n    testWidgets('ascending', (tester) async {\n      final List<String> expectedTiles = [\n        'todoA',\n        'TodoB',\n        'TODOC',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n        filter: const Filter(\n          order: ListOrder.ascending,\n          filter: ListFilter.all,\n          group: ListGroup.none,\n        ),\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n    testWidgets('descending', (tester) async {\n      final List<String> expectedTiles = [\n        'TODOC',\n        'TodoB',\n        'todoA',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n        filter: const Filter(\n          order: ListOrder.descending,\n          filter: ListFilter.all,\n          group: ListGroup.none,\n        ),\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n  });\n\n  group('Filter', () {\n    setUp(() async {\n      file = fs.file('todoFilter.txt');\n      await file.create();\n      await file.writeAsString(\n        [\n          'x 2023-12-04 2023-12-02 TodoC',\n          '2023-12-02 TodoA',\n          'x 2023-12-03 2023-12-02 TodoB',\n        ].join('\\n'),\n        flush: true,\n      );\n    });\n    testWidgets('all', (tester) async {\n      final List<String> expectedTiles = [\n        'TodoA',\n        'TodoB',\n        'TodoC',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n        filter: const Filter(\n          order: ListOrder.ascending,\n          filter: ListFilter.all,\n          group: ListGroup.none,\n        ),\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n    testWidgets('completed only', (tester) async {\n      final List<String> expectedTiles = [\n        'TodoB',\n        'TodoC',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n        filter: const Filter(\n          order: ListOrder.ascending,\n          filter: ListFilter.completedOnly,\n          group: ListGroup.none,\n        ),\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n    testWidgets('incompleted only', (tester) async {\n      final List<String> expectedTiles = [\n        'TodoA',\n      ];\n      await tester.pumpWidget(TodoListPageMaterialApp(\n        localFile: file,\n        filter: const Filter(\n          order: ListOrder.ascending,\n          filter: ListFilter.incompletedOnly,\n          group: ListGroup.none,\n        ),\n      ));\n      await tester.pumpAndSettle();\n\n      Iterable<TodoListTile> todoTiles =\n          tester.widgetList<TodoListTile>(find.byType(TodoListTile));\n      expect(todoTiles.length, expectedTiles.length);\n\n      for (int i = 0; i < expectedTiles.length; i++) {\n        Finder element = find.descendant(\n          of: find.byWidget(todoTiles.elementAt(i)),\n          matching: find.text(expectedTiles[i], findRichText: true),\n        );\n        await tester.ensureVisible(element);\n        expect(element, findsOneWidget);\n      }\n    });\n  });\n\n  group('Group by', () {\n    group('none', () {\n      setUp(() async {\n        file = fs.file('todoGroupByNone.txt');\n        await file.create();\n        await file.writeAsString(\n          [\n            'x 2023-13-04 2023-12-02 TodoB',\n            '2023-12-02 TodoC',\n            '(B) 2023-12-02 TodoA',\n          ].join('\\n'),\n          flush: true,\n        );\n      });\n      testWidgets('ascending', (tester) async {\n        final List<String> expectedTiles = [\n          'All',\n          'TodoA',\n          'TodoC',\n          'TodoB', // Completed todo come always at last.\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.ascending,\n            filter: ListFilter.all,\n            group: ListGroup.none,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n      testWidgets('descending', (tester) async {\n        final List<String> expectedTiles = [\n          'All',\n          'TodoC',\n          'TodoA',\n          'TodoB', // Completed todo come always at last.\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.descending,\n            filter: ListFilter.all,\n            group: ListGroup.none,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n    });\n    group('upcoming', () {\n      setUp(() async {\n        final DateTime now = DateTime.now();\n        final String today =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';\n        final String tomorrow =\n            '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${(now.day + 1).toString().padLeft(2, '0')}';\n\n        file = fs.file('todoGroupByUpcoming.txt');\n        await file.create();\n        await file.writeAsString(\n          [\n            '2023-12-02 TodoA1',\n            'x 2023-12-04 2023-12-02 TodoB2 due:1970-01-01',\n            '2023-12-02 TodoB1 due:1970-01-01',\n            '2023-12-02 TodoC2 due:$today',\n            '2023-12-02 TodoC1 due:$today',\n            '2023-12-02 TodoD2 due:$tomorrow',\n            '(B) 2023-12-02 TodoD1 due:$tomorrow',\n            '2023-12-02 TodoA2',\n          ].join('\\n'),\n          flush: true,\n        );\n      });\n      testWidgets('ascending', (tester) async {\n        // Increase size to ensure all elements in list are visible.\n        tester.view.physicalSize = const Size(800, 1600);\n        tester.view.devicePixelRatio = 1.0;\n\n        final List<String> expectedTiles = [\n          'Deadline passed',\n          'TodoB1',\n          'TodoB2', // Completed todo come always at last.\n          'Today',\n          'TodoC1',\n          'TodoC2',\n          'Upcoming',\n          'TodoD1',\n          'TodoD2',\n          'No deadline',\n          'TodoA1',\n          'TodoA2',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.ascending,\n            filter: ListFilter.all,\n            group: ListGroup.upcoming,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n\n        // resets the screen to its original size after the test end\n        addTearDown(tester.view.resetPhysicalSize);\n        addTearDown(tester.view.resetDevicePixelRatio);\n      });\n      testWidgets('descending', (tester) async {\n        // Increase size to ensure all elements in list are visible.\n        tester.view.physicalSize = const Size(800, 1600);\n        tester.view.devicePixelRatio = 1.0;\n\n        final List<String> expectedTiles = [\n          'Deadline passed',\n          'TodoB1',\n          'TodoB2', // Completed todo come always at last.\n          'Today',\n          'TodoC2',\n          'TodoC1',\n          'Upcoming',\n          'TodoD2',\n          'TodoD1',\n          'No deadline',\n          'TodoA2',\n          'TodoA1',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.descending,\n            filter: ListFilter.all,\n            group: ListGroup.upcoming,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n\n        // resets the screen to its original size after the test end\n        addTearDown(tester.view.resetPhysicalSize);\n        addTearDown(tester.view.resetDevicePixelRatio);\n      });\n    });\n    group('priority', () {\n      setUp(() async {\n        file = fs.file('todoGroupByPriority.txt');\n        await file.create();\n        await file.writeAsString(\n          [\n            '2023-12-02 TodoD',\n            'x 2023-12-04 (B) 2023-12-02 TodoB2',\n            '(B) 2023-12-02 TodoB1',\n            'x 2023-12-04 (A) 2023-12-02 TodoA1',\n            '(A) 2023-12-02 TodoA2',\n            '2023-12-02 TodoC',\n          ].join('\\n'),\n          flush: true,\n        );\n      });\n      testWidgets('ascending', (tester) async {\n        final List<String> expectedTiles = [\n          'A',\n          'TodoA2',\n          'TodoA1', // Completed todo come always at last.\n          'B',\n          'TodoB1',\n          'TodoB2', // Completed todo come always at last.\n          'No priority',\n          'TodoC',\n          'TodoD',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.ascending,\n            filter: ListFilter.all,\n            group: ListGroup.priority,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n      testWidgets('descending', (tester) async {\n        final List<String> expectedTiles = [\n          'No priority',\n          'TodoD',\n          'TodoC',\n          'B',\n          'TodoB1',\n          'TodoB2', // Completed todo come always at last.\n          'A',\n          'TodoA2',\n          'TodoA1', // Completed todo come always at last.\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.descending,\n            filter: ListFilter.all,\n            group: ListGroup.priority,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n    });\n    group('project', () {\n      setUp(() async {\n        file = fs.file('todoGroupByProject.txt');\n        await file.create();\n        await file.writeAsString(\n          [\n            '2023-12-02 TodoD',\n            '(B) 2023-12-02 TodoB2 +project2',\n            '2023-12-02 TodoB1 +project1',\n            'x 2023-13-04 2023-12-02 TodoA1 +project1',\n            '2023-12-02 TodoA2 +project2',\n            '2023-12-02 TodoC',\n          ].join('\\n'),\n          flush: true,\n        );\n      });\n      testWidgets('ascending', (tester) async {\n        final List<String> expectedTiles = [\n          'project1',\n          'TodoB1 +project1',\n          'TodoA1 +project1', // Completed todo come always at last.\n          'project2',\n          'TodoA2 +project2',\n          'TodoB2 +project2',\n          'No project',\n          'TodoC',\n          'TodoD',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.ascending,\n            filter: ListFilter.all,\n            group: ListGroup.project,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n      testWidgets('descending', (tester) async {\n        final List<String> expectedTiles = [\n          'project2',\n          'TodoB2 +project2',\n          'TodoA2 +project2',\n          'project1',\n          'TodoB1 +project1',\n          'TodoA1 +project1', // Completed todo come always at last.\n          'No project',\n          'TodoD',\n          'TodoC',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.descending,\n            filter: ListFilter.all,\n            group: ListGroup.project,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n    });\n    group('context', () {\n      setUp(() async {\n        file = fs.file('todoGroupByContext.txt');\n        await file.create();\n        await file.writeAsString(\n          [\n            '2023-12-02 TodoD',\n            '(B) 2023-12-02 TodoB2 @context2',\n            '2023-12-02 TodoB1 @context1',\n            'x 2023-12-04 2023-12-02 TodoA1 @context1',\n            '2023-12-02 TodoA2 @context2',\n            '2023-12-02 TodoC',\n          ].join('\\n'),\n          flush: true,\n        );\n      });\n      testWidgets('ascending', (tester) async {\n        final List<String> expectedTiles = [\n          'context1',\n          'TodoB1 @context1',\n          'TodoA1 @context1', // Completed todo come always at last.\n          'context2',\n          'TodoA2 @context2',\n          'TodoB2 @context2',\n          'No context',\n          'TodoC',\n          'TodoD',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.ascending,\n            filter: ListFilter.all,\n            group: ListGroup.context,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n      testWidgets('descending', (tester) async {\n        final List<String> expectedTiles = [\n          'context2',\n          'TodoB2 @context2',\n          'TodoA2 @context2',\n          'context1',\n          'TodoB1 @context1',\n          'TodoA1 @context1', // Completed todo come always at last.\n          'No context',\n          'TodoD',\n          'TodoC',\n        ];\n        await tester.pumpWidget(TodoListPageMaterialApp(\n          localFile: file,\n          filter: const Filter(\n            order: ListOrder.descending,\n            filter: ListFilter.all,\n            group: ListGroup.context,\n          ),\n        ));\n        await tester.pumpAndSettle();\n\n        Iterable<ListTile> listTiles =\n            tester.widgetList<ListTile>(find.byType(ListTile));\n        expect(listTiles.length, expectedTiles.length);\n\n        for (int i = 0; i < expectedTiles.length; i++) {\n          Finder element = find.descendant(\n            of: find.byWidget(listTiles.elementAt(i)),\n            matching: find.text(expectedTiles[i], findRichText: true),\n          );\n          await tester.ensureVisible(element);\n          expect(element, findsOneWidget);\n        }\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/state/todo_cubit_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/state/todo_cubit.dart';\nimport 'package:ntodotxt/todo/state/todo_state.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  late Todo todo;\n\n  setUp(() async {});\n\n  group('initial', () {\n    test('initial state', () {\n      todo = Todo(description: 'Write some tests');\n      final TodoCubit todoBloc = TodoCubit(todo: todo);\n      expect(\n        todoBloc.state,\n        TodoSuccess(todo: todo),\n      );\n    });\n  });\n\n  group('completion', () {\n    test('set', () async {\n      todo = Todo(\n        completion: false,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion(completion: true);\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: true,\n            completionDate: DateTime.now(),\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('set with completionDate', () async {\n      todo = Todo(\n        completion: false,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion(\n        completion: true,\n        completionDate: DateTime(2025, 1, 1),\n      );\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: true,\n            completionDate: DateTime(2025, 1, 1),\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('set with completionDate and incorrectly set completion attribute',\n        () async {\n      todo = Todo(\n        completion: false,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion(\n        completion: false,\n        completionDate: DateTime(2025, 1, 1),\n      );\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: true,\n            completionDate: DateTime(2025, 1, 1),\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test(\n        'set with completionDate and without explicitly set completion attribute',\n        () async {\n      todo = Todo(\n        completion: false,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion(completionDate: DateTime(2025, 1, 1));\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: true,\n            completionDate: DateTime(2025, 1, 1),\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('unset', () async {\n      todo = Todo(\n        completion: true,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion(completion: false);\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: false,\n            completionDate: null,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('toggle true', () async {\n      todo = Todo(\n        completion: false,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion();\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: true,\n            completionDate: DateTime.now(),\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('toggle false', () async {\n      todo = Todo(\n        completion: true,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.toggleCompletion();\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            completion: false,\n            completionDate: null,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n  });\n\n  group('description', () {\n    test('set', () async {\n      todo = Todo(description: 'Write some tests');\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.updateDescription('Write more tests');\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            description: 'Write more tests',\n          ),\n        ),\n      );\n    });\n  });\n\n  group('priority', () {\n    test('set', () async {\n      todo = Todo(\n        priority: Priority.none,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.setPriority(Priority.A);\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            priority: Priority.A,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('update', () async {\n      todo = Todo(\n        priority: Priority.A,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.setPriority(Priority.B);\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            priority: Priority.B,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('unset', () async {\n      todo = Todo(\n        priority: Priority.A,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.unsetPriority();\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            priority: Priority.none,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n    test('unset (already unset)', () async {\n      todo = Todo(\n        priority: Priority.none,\n        description: 'Write some tests',\n      );\n      final TodoCubit bloc = TodoCubit(todo: todo);\n      bloc.unsetPriority();\n\n      expect(\n        bloc.state,\n        TodoSuccess(\n          todo: Todo(\n            priority: Priority.none,\n            description: todo.description,\n          ),\n        ),\n      );\n    });\n  });\n\n  group('projects', () {\n    group('add', () {\n      test('initial', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addProject('project1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1',\n            ),\n          ),\n        );\n      });\n      test('additional', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addProject('project2');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1 +project2',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addProject('project 2');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid project tag: project 2',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('duplication', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addProject('project1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1',\n            ),\n          ),\n        );\n      });\n      test('no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addProject('Project1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1 +Project1',\n            ),\n          ),\n        );\n      });\n    });\n    group('update', () {\n      test('multiple entries', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateProjects({'project1', 'project2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1 +project2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries (add & remove)', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateProjects({'project2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateProjects({'project1', 'project 2'});\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid project tag: project 2',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / duplication', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateProjects({'project1', 'project2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1 +project2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateProjects({'project1', 'Project1', 'Project2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests +project1 +Project1 +Project2',\n            ),\n          ),\n        );\n      });\n    });\n    group('remove', () {\n      test('initial', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeProject('project1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests +project1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeProject('project 1');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid project tag: project 1',\n            todo: Todo(\n              description: 'Write some tests +project1',\n            ),\n          ),\n        );\n      });\n      test('not exists', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeProject('project1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n    });\n  });\n\n  group('contexts', () {\n    group('add', () {\n      test('initial', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addContext('context1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1',\n            ),\n          ),\n        );\n      });\n      test('additional', () async {\n        todo = Todo(description: 'Write some tests @context1 @context2');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addContext('context2');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1 @context2',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addContext('context 2');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid context tag: context 2',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('duplication', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addContext('context1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1',\n            ),\n          ),\n        );\n      });\n      test('no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addContext('Context1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1 @Context1',\n            ),\n          ),\n        );\n      });\n    });\n    group('update', () {\n      test('multiple entries', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateContexts({'context1', 'context2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1 @context2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries (add & remove)', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateContexts({'context2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateContexts({'context1', 'context 2'});\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid context tag: context 2',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / duplication', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateContexts({'context1', 'context2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1 @context2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateContexts({'context1', 'Context1', 'Context2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests @context1 @Context1 @Context2',\n            ),\n          ),\n        );\n      });\n    });\n    group('remove', () {\n      test('initial', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeContext('context1');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests @context1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeContext('context 1');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid context tag: context 1',\n            todo: Todo(\n              description: 'Write some tests @context1',\n            ),\n          ),\n        );\n      });\n      test('not exists', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeContext('context2');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n    });\n  });\n\n  group('key values', () {\n    group('add', () {\n      test('initial', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('key:val');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests key:val',\n            ),\n          ),\n        );\n      });\n      test('additional', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('key:val');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:bar key:val',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('key_val');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid key value tag: key_val',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('duplication', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('foo:bar');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:bar',\n            ),\n          ),\n        );\n      });\n      test('no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('Foo:bar');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:bar Foo:bar',\n            ),\n          ),\n        );\n      });\n      test('duplication / update value', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.addKeyValue('foo:new');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:new',\n            ),\n          ),\n        );\n      });\n    });\n    group('update', () {\n      test('multiple entries', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'key1:val1', 'key2:val2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests key1:val1 key2:val2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries (add & remove)', () async {\n        todo = Todo(description: 'Write some tests key1:val1');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'key2:val2'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests key2:val2',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / invalid format', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'key1:val1', 'key2_val2'});\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid key value tag: key2_val2',\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / duplication', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'key1:val1', 'foo:bar'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:bar key1:val1',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / no duplication / case insensitive', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'foo:bar', 'Key1:val1', 'Foo:bar'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:bar Key1:val1 Foo:bar',\n            ),\n          ),\n        );\n      });\n      test('multiple entries / update value', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.updateKeyValues({'key1:val1', 'foo:new'});\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests foo:new key1:val1',\n            ),\n          ),\n        );\n      });\n    });\n    group('remove', () {\n      test('add', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeKeyValue('foo:bar');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n      test('invalid format', () async {\n        todo = Todo(description: 'Write some tests foo:bar');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeKeyValue('key_val');\n\n        expect(\n          bloc.state,\n          TodoError(\n            message: 'Invalid key value tag: key_val',\n            todo: Todo(\n              description: 'Write some tests foo:bar',\n            ),\n          ),\n        );\n      });\n      test('not exits', () async {\n        todo = Todo(description: 'Write some tests');\n        final TodoCubit bloc = TodoCubit(todo: todo);\n        bloc.removeKeyValue('key:val');\n\n        expect(\n          bloc.state,\n          TodoSuccess(\n            todo: Todo(\n              description: 'Write some tests',\n            ),\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo/state/todo_list_bloc_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:file/memory.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/todo/api/todo_list_api.dart';\nimport 'package:ntodotxt/todo/model/todo_model.dart';\nimport 'package:ntodotxt/todo/repository/todo_list_repository.dart';\nimport 'package:ntodotxt/todo/state/todo_list_bloc.dart';\nimport 'package:ntodotxt/todo/state/todo_list_event.dart';\nimport 'package:ntodotxt/todo/state/todo_list_state.dart';\n\nFile mockTodoListFile(List<String> rawTodoList) {\n  final MemoryFileSystem fs = MemoryFileSystem();\n  final File file = fs.file('todo.txt');\n  file.createSync();\n  file.writeAsStringSync(\n    rawTodoList.join(Platform.lineTerminator),\n    flush: true,\n  );\n\n  return file;\n}\n\nTodoListRepository mockLocalTodoListRepository(File todoFile) {\n  final LocalTodoListApi api = LocalTodoListApi.fromFile(localFile: todoFile);\n  final TodoListRepository repository = TodoListRepository(api);\n\n  return repository;\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  late File todoFile;\n  late TodoListRepository repository;\n  late Todo todo;\n\n  setUp(() {\n    todoFile = mockTodoListFile([]);\n    repository = mockLocalTodoListRepository(todoFile);\n  });\n\n  group('Initial', () {\n    test('initial state', () {\n      final TodoListBloc todoListBloc = TodoListBloc(repository: repository);\n      expect(todoListBloc.state is TodoListLoading, true);\n      expect(todoListBloc.state.todoList, []);\n    });\n  });\n\n  group('TodoListSynchronizationRequested', () {\n    test('initial state', () async {\n      final TodoListBloc bloc = TodoListBloc(repository: repository);\n      bloc\n        ..add(const TodoListSubscriptionRequested())\n        ..add(const TodoListSynchronizationRequested());\n\n      await expectLater(\n        bloc.stream,\n        emitsInOrder(\n          [\n            emitsThrough(\n              const TodoListSuccess(todoList: []),\n            ),\n          ],\n        ),\n      );\n    });\n  });\n\n  group('TodoListTodoSubmitted', () {\n    group('completion', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          completion: false,\n          description: 'Write some tests',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(completion: true),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(completion: true)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('priority', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          priority: Priority.A,\n          description: 'Write some tests',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(priority: Priority.B),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsThrough(\n            TodoListSuccess(\n              todoList: [todo.copyWith(priority: Priority.B)],\n            ),\n          ),\n        );\n      });\n      test('unset', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(priority: Priority.none),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(priority: Priority.none)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('description', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          description: 'Write some tests',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(description: 'Write more tests'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(description: 'Write more tests')],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('projects', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          description: 'Write some tests +project1',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(\n                  description: 'Write some tests +project1 +project2'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests +project1 +project2',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n      test('unset', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(description: 'Write some tests'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('contexts', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          description: 'Write some tests @context1',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(\n                  description: 'Write some tests @context1 @context2'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests @context1 @context2',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n      test('unset', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(description: 'Write some tests'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('key values', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          description: 'Write some tests foo:bar',\n        );\n      });\n      test('set', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(\n                  description: 'Write some tests foo:bar key:val'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests foo:bar key:val',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n      test('unset', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo.copyWith(description: 'Write some tests'),\n            ),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [\n                    todo.copyWith(\n                      description: 'Write some tests',\n                    )\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n  });\n\n  group('TodoListTodoDeleted', () {\n    setUp(() async {\n      todo = Todo(\n        id: '1',\n        description: 'Write some tests',\n      );\n    });\n    test('delete existing', () async {\n      final TodoListBloc bloc = TodoListBloc(repository: repository);\n      bloc\n        ..add(const TodoListSubscriptionRequested())\n        ..add(\n          TodoListTodoSubmitted(\n            todo: todo,\n          ),\n        )\n        ..add(\n          TodoListTodoDeleted(todo: todo),\n        );\n\n      await expectLater(\n        bloc.stream,\n        emitsInOrder(\n          [\n            emitsThrough(\n              const TodoListSuccess(todoList: []),\n            ),\n          ],\n        ),\n      );\n    });\n    test('delete non-existing', () async {\n      final TodoListBloc bloc = TodoListBloc(repository: repository);\n      bloc\n        ..add(const TodoListSubscriptionRequested())\n        ..add(\n          TodoListTodoSubmitted(\n            todo: todo,\n          ),\n        )\n        ..add(\n          TodoListTodoDeleted(\n            todo: Todo(\n              id: '2',\n              description: 'Write some tests!!!',\n            ),\n          ),\n        );\n\n      await expectLater(\n        bloc.stream,\n        emitsInOrder(\n          [\n            emitsThrough(\n              TodoListSuccess(\n                todoList: [\n                  todo,\n                ],\n              ),\n            ),\n          ],\n        ),\n      );\n    });\n  });\n\n  group('TodoListTodoCompletionToggled', () {\n    group('toggle to completed', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          description: 'Write some tests',\n        );\n      });\n      test('toggle existing', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoCompletionToggled(todo: todo, completion: true),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(completion: true)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n      test('toggle non-existing', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoCompletionToggled(todo: todo, completion: true),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(completion: true)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n\n    group('toggle to incompleted', () {\n      setUp(() async {\n        todo = Todo(\n          id: '1',\n          completion: true,\n          description: 'Write some tests',\n        );\n      });\n      test('toggle existing', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoSubmitted(\n              todo: todo,\n            ),\n          )\n          ..add(\n            TodoListTodoCompletionToggled(todo: todo, completion: false),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(completion: false)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n      test('toggle non-existing', () async {\n        final TodoListBloc bloc = TodoListBloc(repository: repository);\n        bloc\n          ..add(const TodoListSubscriptionRequested())\n          ..add(\n            TodoListTodoCompletionToggled(todo: todo, completion: false),\n          );\n\n        await expectLater(\n          bloc.stream,\n          emitsInOrder(\n            [\n              emitsThrough(\n                TodoListSuccess(\n                  todoList: [todo.copyWith(completion: false)],\n                ),\n              ),\n            ],\n          ),\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "test/todo_file/state/todo_file_cubit_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/setting/controller/fake_setting_controller.dart';\nimport 'package:ntodotxt/setting/repository/setting_repository.dart'\n    show SettingRepository;\nimport 'package:ntodotxt/setting/repository/setting_repository.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_cubit.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\n\nvoid main() {\n  group('TodoFileCubit', () {\n    test('Default values', () {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      expect(cubit.state, isA<TodoFileLoading>());\n      expect(cubit.state.todoFilename, 'todo.txt');\n      expect(cubit.state.doneFilename, 'done.txt');\n      expect(cubit.state.localPath, '/');\n      expect(cubit.state.remotePath, '/');\n    });\n  });\n  group('saveLocalPath', () {\n    test('Null value', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveLocalPath(null);\n      expect(cubit.state.localPath, '/');\n    });\n    test('Without trailing /', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveLocalPath('/local');\n      expect(cubit.state.localPath, '/local/');\n    });\n    test('With trailing /', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveLocalPath('/local/');\n      expect(cubit.state.localPath, '/local/');\n    });\n  });\n  group('saveRemotePath', () {\n    test('Null value', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveRemotePath(null);\n      expect(cubit.state.remotePath, '/');\n    });\n    test('Without trailing /', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveRemotePath('/remote');\n      expect(cubit.state.remotePath, '/remote/');\n    });\n    test('With trailing /', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveRemotePath('/remote/');\n      expect(cubit.state.remotePath, '/remote/');\n    });\n  });\n  group('saveLocalFilename', () {\n    test('Null value', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveLocalFilename(null);\n      expect(cubit.state.todoFilename, 'todo.txt');\n    });\n    test('With value', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n      );\n      await cubit.saveLocalFilename('todo2.txt');\n      expect(cubit.state.todoFilename, 'todo2.txt');\n    });\n  });\n  group('resetToDefaults', () {\n    test('Reset to defaults', () async {\n      final TodoFileCubit cubit = TodoFileCubit(\n        repository: SettingRepository(FakeSettingController()),\n        todoFilename: 'todo2.txt',\n        doneFilename: 'done2.txt',\n        localPath: '/local',\n        remotePath: '/remote',\n      );\n      await cubit.resetToDefaults();\n      expect(cubit.state, isA<TodoFileLoading>());\n      expect(cubit.state.todoFilename, 'todo.txt');\n      expect(cubit.state.doneFilename, 'done.txt');\n      expect(cubit.state.localPath, '/');\n      expect(cubit.state.remotePath, '/');\n    });\n  });\n  group('resetTodoFileSettings', () {});\n}\n"
  },
  {
    "path": "test/todo_file/state/todo_file_state_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/todo_file/state/todo_file_state.dart';\n\nvoid main() {\n  group('TodoFileLoading', () {\n    test('localPath with trailing /', () {\n      TodoFileLoading state = TodoFileLoading(localPath: 'path/');\n      expect(state.localPath, 'path/');\n    });\n    test('localPath without trailing /', () {\n      TodoFileLoading state = TodoFileLoading(localPath: 'path');\n      expect(state.localPath, 'path/');\n    });\n    test('remotePath with trailing /', () {\n      TodoFileLoading state = TodoFileLoading(remotePath: 'path/');\n      expect(state.remotePath, 'path/');\n    });\n    test('remotePath without trailing /', () {\n      TodoFileLoading state = TodoFileLoading(remotePath: 'path');\n      expect(state.remotePath, 'path/');\n    });\n    test('copyWith() with todoFilename', () {\n      TodoFileLoading state = TodoFileLoading(\n        todoFilename: 'todo.txt',\n      );\n      TodoFileLoading state2 = state.copyWith(todoFilename: 'todo2.txt');\n      expect(state2.todoFilename, 'todo2.txt');\n    });\n    test('copyWith() with doneFilename', () {\n      TodoFileLoading state = TodoFileLoading(\n        doneFilename: 'done.txt',\n      );\n      TodoFileLoading state2 = state.copyWith(doneFilename: 'done2.txt');\n      expect(state2.doneFilename, 'done2.txt');\n    });\n    test('copyWith() with localPath', () {\n      TodoFileLoading state = TodoFileLoading(\n        localPath: '/local',\n      );\n      TodoFileLoading state2 = state.copyWith(localPath: '/local2/');\n      expect(state2.localPath, '/local2/');\n    });\n    test('copyWith() with remotePath', () {\n      TodoFileLoading state = TodoFileLoading(\n        remotePath: '/remote',\n      );\n      TodoFileLoading state2 = state.copyWith(remotePath: '/remote2/');\n      expect(state2.remotePath, '/remote2/');\n    });\n    test('toString() without trailing /', () {\n      TodoFileLoading state = TodoFileLoading(\n        todoFilename: 'todo.txt',\n        localPath: '/local',\n        remotePath: '/remote',\n      );\n      expect('$state',\n          'TodoFileLoading { localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('toString() with trailing /', () {\n      TodoFileLoading state = TodoFileLoading(\n        todoFilename: 'todo.txt',\n        localPath: '/local/',\n        remotePath: '/remote/',\n      );\n      expect('$state',\n          'TodoFileLoading { localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('load()', () {\n      TodoFileLoading state = TodoFileLoading();\n      TodoFileLoading state2 = state.load();\n      expect(state2, isA<TodoFileLoading>());\n    });\n    test('ready()', () {\n      TodoFileLoading state = TodoFileLoading();\n      TodoFileReady state2 = state.ready();\n      expect(state2, isA<TodoFileReady>());\n    });\n    test('error()', () {\n      TodoFileLoading state = TodoFileLoading();\n      TodoFileError state2 = state.error(message: 'error');\n      expect(state2, isA<TodoFileError>());\n    });\n  });\n\n  group('TodoFileReady', () {\n    test('localPath with trailing /', () {\n      TodoFileReady state = TodoFileReady(localPath: 'path/');\n      expect(state.localPath, 'path/');\n    });\n    test('localPath without trailing /', () {\n      TodoFileReady state = TodoFileReady(localPath: 'path');\n      expect(state.localPath, 'path/');\n    });\n    test('remotePath with trailing /', () {\n      TodoFileReady state = TodoFileReady(remotePath: 'path/');\n      expect(state.remotePath, 'path/');\n    });\n    test('remotePath without trailing /', () {\n      TodoFileReady state = TodoFileReady(remotePath: 'path');\n      expect(state.remotePath, 'path/');\n    });\n    test('copyWith() with todoFilename', () {\n      TodoFileReady state = TodoFileReady(\n        todoFilename: 'todo.txt',\n      );\n      TodoFileReady state2 = state.copyWith(todoFilename: 'todo2.txt');\n      expect(state2.todoFilename, 'todo2.txt');\n    });\n    test('copyWith() with doneFilename', () {\n      TodoFileReady state = TodoFileReady(\n        doneFilename: 'done.txt',\n      );\n      TodoFileReady state2 = state.copyWith(doneFilename: 'done2.txt');\n      expect(state2.doneFilename, 'done2.txt');\n    });\n    test('copyWith() with localPath', () {\n      TodoFileReady state = TodoFileReady(\n        localPath: '/local',\n      );\n      TodoFileReady state2 = state.copyWith(localPath: '/local2/');\n      expect(state2.localPath, '/local2/');\n    });\n    test('copyWith() with remotePath', () {\n      TodoFileReady state = TodoFileReady(\n        remotePath: '/remote',\n      );\n      TodoFileReady state2 = state.copyWith(remotePath: '/remote2/');\n      expect(state2.remotePath, '/remote2/');\n    });\n    test('toString() without trailing /', () {\n      TodoFileReady state = TodoFileReady(\n        todoFilename: 'todo.txt',\n        localPath: '/local',\n        remotePath: '/remote',\n      );\n      expect('$state',\n          'TodoFileReady { localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('toString() with trailing /', () {\n      TodoFileReady state = TodoFileReady(\n        todoFilename: 'todo.txt',\n        localPath: '/local/',\n        remotePath: '/remote/',\n      );\n      expect('$state',\n          'TodoFileReady { localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('load()', () {\n      TodoFileReady state = TodoFileReady();\n      TodoFileLoading state2 = state.load();\n      expect(state2, isA<TodoFileLoading>());\n    });\n    test('ready()', () {\n      TodoFileReady state = TodoFileReady();\n      TodoFileReady state2 = state.ready();\n      expect(state2, isA<TodoFileReady>());\n    });\n    test('error()', () {\n      TodoFileReady state = TodoFileReady();\n      TodoFileError state2 = state.error(message: 'error');\n      expect(state2, isA<TodoFileError>());\n    });\n  });\n\n  group('TodoFileError', () {\n    test('localPath with trailing /', () {\n      TodoFileError state = TodoFileError(localPath: 'path/', message: '');\n      expect(state.localPath, 'path/');\n    });\n    test('localPath without trailing /', () {\n      TodoFileError state = TodoFileError(localPath: 'path', message: '');\n      expect(state.localPath, 'path/');\n    });\n    test('remotePath with trailing /', () {\n      TodoFileError state = TodoFileError(remotePath: 'path/', message: '');\n      expect(state.remotePath, 'path/');\n    });\n    test('remotePath without trailing /', () {\n      TodoFileError state = TodoFileError(remotePath: 'path', message: '');\n      expect(state.remotePath, 'path/');\n    });\n    test('copyWith() with todoFilename', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        todoFilename: 'todo.txt',\n      );\n      TodoFileError state2 = state.copyWith(todoFilename: 'todo2.txt');\n      expect(state2.todoFilename, 'todo2.txt');\n    });\n    test('copyWith() with doneFilename', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        doneFilename: 'done.txt',\n      );\n      TodoFileError state2 = state.copyWith(doneFilename: 'done2.txt');\n      expect(state2.doneFilename, 'done2.txt');\n    });\n    test('copyWith() with localPath', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        localPath: '/local',\n      );\n      TodoFileError state2 = state.copyWith(localPath: '/local2/');\n      expect(state2.localPath, '/local2/');\n    });\n    test('copyWith() with remotePath', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        remotePath: '/remote',\n      );\n      TodoFileError state2 = state.copyWith(remotePath: '/remote2/');\n      expect(state2.remotePath, '/remote2/');\n    });\n    test('toString() without trailing /', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        todoFilename: 'todo.txt',\n        localPath: '/local',\n        remotePath: '/remote',\n      );\n      expect('$state',\n          'TodoFileError { message error localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('toString() with trailing /', () {\n      TodoFileError state = TodoFileError(\n        message: 'error',\n        todoFilename: 'todo.txt',\n        localPath: '/local/',\n        remotePath: '/remote/',\n      );\n      expect('$state',\n          'TodoFileError { message error localFile /local/todo.txt remoteFile: /remote/todo.txt }');\n    });\n    test('load()', () {\n      TodoFileError state = TodoFileError(message: 'error');\n      TodoFileLoading state2 = state.load();\n      expect(state2, isA<TodoFileLoading>());\n    });\n    test('ready()', () {\n      TodoFileError state = TodoFileError(message: 'error');\n      TodoFileReady state2 = state.ready();\n      expect(state2, isA<TodoFileReady>());\n    });\n    test('error()', () {\n      TodoFileError state = TodoFileError(message: 'error');\n      TodoFileError state2 = state.error(message: 'error');\n      expect(state2, isA<TodoFileError>());\n    });\n  });\n}\n"
  },
  {
    "path": "test/webdav/client/webdav_client_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:ntodotxt/webdav/client/webdav_client.dart';\n\nvoid main() {\n  setUp(() {});\n\n  group('scheme', () {\n    test('http', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.scheme, 'http');\n    });\n    test('https', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'https://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.scheme, 'https');\n    });\n    test('undefined', () {\n      expect(\n        () => WebDAVClient(\n          server: 'example.org',\n          path: '/',\n          username: 'test',\n          password: 'test',\n        ),\n        throwsA(isA<FormatException>()),\n      );\n    });\n    test('http', () {\n      expect(\n        () => WebDAVClient(\n          server: 'httpx://example.org',\n          path: '/',\n          username: 'test',\n          password: 'test',\n        ),\n        throwsA(isA<FormatException>()),\n      );\n    });\n  });\n\n  group('host', () {\n    test('valid 1', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.host, 'example.org');\n    });\n    test('valid 2', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example-local',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.host, 'example-local');\n    });\n    test('undefined', () {\n      expect(\n        () => WebDAVClient(\n          server: 'httpx://',\n          path: '/',\n          username: 'test',\n          password: 'test',\n        ),\n        throwsA(isA<FormatException>()),\n      );\n    });\n    test('invalid', () {\n      expect(\n        () => WebDAVClient(\n          server: 'httpx://example_test.org',\n          path: '/',\n          username: 'test',\n          password: 'test',\n        ),\n        throwsA(isA<FormatException>()),\n      );\n    });\n  });\n\n  group('port', () {\n    test('valid 1', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org:80',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.port, 80);\n    });\n    test('valid 2', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org:8443',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.port, 8443);\n    });\n    test('undefined', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.port, null);\n    });\n    test('invalid', () {\n      expect(\n        () => WebDAVClient(\n          server: 'httpx://example.org:abc',\n          path: '/',\n          username: 'test',\n          password: 'test',\n        ),\n        throwsA(isA<FormatException>()),\n      );\n    });\n  });\n\n  group('path', () {\n    test('valid 1', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.path, '');\n    });\n    test('valid 2', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.path, '/');\n    });\n    test('valid 3', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/test',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.path, '/test');\n    });\n    test('valid 4', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/test/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.path, '/test');\n    });\n  });\n\n  group('username', () {\n    test('valid', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.username, 'test');\n    });\n  });\n\n  group('password', () {\n    test('valid', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.password, 'test');\n    });\n  });\n\n  group('acceptUntrustedCert', () {\n    test('true', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n        acceptUntrustedCert: true,\n      );\n      expect(client.acceptUntrustedCert, true);\n    });\n    test('false', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n        acceptUntrustedCert: false,\n      );\n      expect(client.acceptUntrustedCert, false);\n    });\n    test('undefined', () {\n      WebDAVClient client = WebDAVClient(\n        server: 'http://example.org',\n        path: '/',\n        username: 'test',\n        password: 'test',\n      );\n      expect(client.acceptUntrustedCert, false);\n    });\n  });\n}\n"
  },
  {
    "path": "test_driver/screenshot_integration_test.dart",
    "content": "import 'dart:io';\nimport 'package:integration_test/integration_test_driver_extended.dart';\n\nFuture<void> main() async {\n  try {\n    await integrationDriver(\n      onScreenshot: (String screenshotName, List<int> screenshotBytes,\n          [Map<String, Object?>? args]) async {\n        final File image = await File('screenshots/$screenshotName.png')\n            .create(recursive: true);\n        image.writeAsBytesSync(screenshotBytes);\n        return true;\n      },\n    );\n  } catch (e) {\n    // ignore: avoid_print\n    print('Error occured: $e');\n  }\n}\n"
  },
  {
    "path": "web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"ntodotxt\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>ntodotxt</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <script src=\"flutter_bootstrap.js\" async></script>\n</body>\n</html>\n"
  },
  {
    "path": "web/manifest.json",
    "content": "{\n    \"name\": \"ntodotxt\",\n    \"short_name\": \"ntodotxt\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ]\n}\n"
  },
  {
    "path": "windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(ntodotxt 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 \"ntodotxt\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(VERSION 3.14...3.25)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/windows/\")\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.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"de.tnmgl\" \"\\0\"\n            VALUE \"FileDescription\", \"ntodotxt\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"ntodotxt\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2025 de.tnmgl. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"ntodotxt.exe\" \"\\0\"\n            VALUE \"ProductName\", \"ntodotxt\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    this->Show();\n  });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.Create(L\"ntodotxt\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 and Windows 11 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n    </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  unsigned int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr)\n    -1; // remove the trailing null character\n  int input_length = (int)wcslen(utf16_string);\n  std::string utf8_string;\n  if (target_length == 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      input_length, utf8_string.data(), target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "path": "windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]