Repository: tmaegel/ntodotxt Branch: main Commit: 8a3f0c0829bf Files: 206 Total size: 1.1 MB Directory structure: gitextract_y8tbwi3d/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── .metadata ├── .pre-commit-config.yaml ├── .vscode/ │ └── tasks.json ├── .yamlfmt ├── CHANGELOG.md ├── Caddyfile ├── Dockerfile_fdroid ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ └── src/ │ │ ├── debug/ │ │ │ └── AndroidManifest.xml │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── de/ │ │ │ │ └── tnmgl/ │ │ │ │ └── ntodotxt/ │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── launch_background.xml │ │ │ ├── drawable-v21/ │ │ │ │ └── launch_background.xml │ │ │ ├── values/ │ │ │ │ └── styles.xml │ │ │ └── values-night/ │ │ │ └── styles.xml │ │ └── profile/ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ └── settings.gradle ├── docker-compose.yaml ├── emulatorctl ├── fonts/ │ └── LICENSE ├── integration_test/ │ ├── login/ │ │ └── login_integration_test.dart │ ├── preview_app_integration_test.dart │ ├── screenshot_integration_test.dart │ └── webdav/ │ └── client/ │ └── webdav_client_test.dart ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ ├── Contents.json │ │ │ └── README.md │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── RunnerTests/ │ └── RunnerTests.swift ├── lib/ │ ├── adaptive_layout/ │ │ └── widget/ │ │ └── adaptive_layout.dart │ ├── app_info/ │ │ └── page/ │ │ └── app_details_page.dart │ ├── common/ │ │ ├── bloc_observer.dart │ │ ├── constants/ │ │ │ └── app.dart │ │ ├── exception/ │ │ │ └── exceptions.dart │ │ ├── misc.dart │ │ ├── router/ │ │ │ └── router.dart │ │ ├── theme/ │ │ │ └── theme.dart │ │ └── widget/ │ │ ├── app_bar.dart │ │ ├── chip.dart │ │ ├── confirm_dialog.dart │ │ ├── contexts_dialog.dart │ │ ├── date_picker.dart │ │ ├── filter_dialog.dart │ │ ├── group_by_dialog.dart │ │ ├── info_dialog.dart │ │ ├── input_dialog.dart │ │ ├── key_values_dialog.dart │ │ ├── order_dialog.dart │ │ ├── priorities_dialog.dart │ │ ├── projects_dialog.dart │ │ ├── scroll_to_top.dart │ │ └── tag_dialog.dart │ ├── database/ │ │ └── controller/ │ │ └── database.dart │ ├── drawer/ │ │ ├── state/ │ │ │ ├── drawer_cubit.dart │ │ │ └── drawer_state.dart │ │ └── widget/ │ │ └── drawer.dart │ ├── filter/ │ │ ├── controller/ │ │ │ ├── fake_filter_controller.dart │ │ │ └── filter_controller.dart │ │ ├── model/ │ │ │ └── filter_model.dart │ │ ├── page/ │ │ │ ├── filter_create_edit_page.dart │ │ │ └── filter_list_page.dart │ │ ├── repository/ │ │ │ └── filter_repository.dart │ │ ├── state/ │ │ │ ├── filter_cubit.dart │ │ │ ├── filter_list_bloc.dart │ │ │ ├── filter_list_event.dart │ │ │ ├── filter_list_state.dart │ │ │ └── filter_state.dart │ │ └── widget/ │ │ └── filter_chip.dart │ ├── intro/ │ │ └── page/ │ │ └── intro_page.dart │ ├── licenses/ │ │ └── page/ │ │ └── licenses_page.dart │ ├── login/ │ │ ├── page/ │ │ │ └── login_page.dart │ │ └── state/ │ │ ├── login_cubit.dart │ │ └── login_state.dart │ ├── main.dart │ ├── oss_licenses.dart │ ├── setting/ │ │ ├── controller/ │ │ │ ├── fake_setting_controller.dart │ │ │ └── setting_controller.dart │ │ ├── model/ │ │ │ └── setting_model.dart │ │ ├── page/ │ │ │ └── settings_page.dart │ │ ├── repository/ │ │ │ └── setting_repository.dart │ │ └── state/ │ │ ├── interaction_settings_cubit.dart │ │ └── interaction_settings_state.dart │ ├── todo/ │ │ ├── api/ │ │ │ └── todo_list_api.dart │ │ ├── model/ │ │ │ └── todo_model.dart │ │ ├── page/ │ │ │ ├── todo_create_edit_page.dart │ │ │ ├── todo_list_page.dart │ │ │ └── todo_search_page.dart │ │ ├── repository/ │ │ │ └── todo_list_repository.dart │ │ └── state/ │ │ ├── todo_cubit.dart │ │ ├── todo_list_bloc.dart │ │ ├── todo_list_event.dart │ │ ├── todo_list_state.dart │ │ └── todo_state.dart │ ├── todo_file/ │ │ └── state/ │ │ ├── todo_file_cubit.dart │ │ └── todo_file_state.dart │ └── webdav/ │ └── client/ │ └── webdav_client.dart ├── linux/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ └── CMakeLists.txt │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── Flutter-Debug.xcconfig │ │ └── Flutter-Release.xcconfig │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Configs/ │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── RunnerTests/ │ └── RunnerTests.swift ├── metadata/ │ └── en-US/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── ntodotxt.yaml ├── pubspec.yaml ├── test/ │ ├── common/ │ │ └── widget/ │ │ ├── confirm_dialog_test.dart │ │ ├── contexts_dialog_test.dart │ │ ├── default_filter_state_filter_dialog_test.dart │ │ ├── default_filter_state_group_dialog_test.dart │ │ ├── default_filter_state_order_dialog_test.dart │ │ ├── filter_state_filter_dialog_test.dart │ │ ├── filter_state_group_dialog_test.dart │ │ ├── filter_state_order_dialog_test.dart │ │ ├── info_dialog_test.dart │ │ ├── input_dialog_test.dart │ │ ├── key_values_dialog_test.dart │ │ ├── priorities_dialog_test.dart │ │ └── projects_dialog_test.dart │ ├── drawer/ │ │ └── state/ │ │ └── drawer_cubit_test.dart │ ├── filter/ │ │ ├── controller/ │ │ │ └── filter_controller_test.dart │ │ ├── page/ │ │ │ ├── filter_create_edit_page_test.dart │ │ │ └── filter_list_page_test.dart │ │ ├── state/ │ │ │ └── filter_cubit_test.dart │ │ └── widget/ │ │ └── filter_chip_test.dart │ ├── login/ │ │ └── page/ │ │ └── webdav_login_view_test.dart │ ├── setting/ │ │ ├── controller/ │ │ │ └── setting_controller_test.dart │ │ └── page/ │ │ └── settings_page_test.dart │ ├── todo/ │ │ ├── api/ │ │ │ └── todo_list_api_test.dart │ │ ├── model/ │ │ │ └── todo_model_test.dart │ │ ├── page/ │ │ │ ├── todo_create_edit_page_test.dart │ │ │ └── todo_list_page_test.dart │ │ └── state/ │ │ ├── todo_cubit_test.dart │ │ └── todo_list_bloc_test.dart │ ├── todo_file/ │ │ └── state/ │ │ ├── todo_file_cubit_test.dart │ │ └── todo_file_state_test.dart │ └── webdav/ │ └── client/ │ └── webdav_client_test.dart ├── test_driver/ │ └── screenshot_integration_test.dart ├── web/ │ ├── index.html │ └── manifest.json └── windows/ ├── .gitignore ├── CMakeLists.txt ├── flutter/ │ └── CMakeLists.txt └── runner/ ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 ENV FLUTTER_CHANNEL="stable" ENV FLUTTER_VERSION="3.24.5" ENV FLUTTER_HOME="/home/vscode/flutter" ENV ANDROID_HOME="/home/vscode/.android-sdk" ENV ANDROID_USER_HOME="/home/vscode/.android" ENV ANDROID_PLATFORM_VERSION="36" ENV ANDROID_BUILD_TOOLS_VERSION="36.0.0" ENV ANDROID_SYSTEM_IMAGE="system-images;android-${ANDROID_PLATFORM_VERSION};google_apis;x86_64" ENV DEBIAN_FRONTEND="noninteractive" ENV PATH=${PATH}:${FLUTTER_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator RUN apt-get update && \ apt-get install -y --no-install-recommends \ clang \ cmake \ curl \ dconf-gsettings-backend \ dconf-service \ gsettings-desktop-schemas \ libcanberra-gtk-module \ libgl1-mesa-dri \ libglu1-mesa \ libgtk-3-dev \ liblzma-dev \ libpulse-dev \ libsecret-1-dev \ libsqlite3-dev \ libstdc++-12-dev \ libx11-xcb1 \ libxcb-cursor0 \ libxcb-xinerama0 \ libxkbcommon-x11-0 \ libxkbfile-dev \ ninja-build \ openjdk-17-jdk-headless \ pkg-config \ unzip \ vim \ wget \ xdg-user-dirs \ xz-utils \ zip && \ apt-get clean -y && \ rm -rf /var/lib/apt/lists/* RUN mkdir -p ${ANDROID_USER_HOME} ${ANDROID_HOME}/cmdline-tools && \ wget "https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip" -O commandlinetools.zip && \ unzip commandlinetools.zip -d ${ANDROID_HOME}/cmdline-tools && \ mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest && \ rm commandlinetools.zip RUN yes | sdkmanager --licenses && \ sdkmanager --update && \ sdkmanager platform-tools && \ sdkmanager emulator && \ sdkmanager "platforms;android-${ANDROID_PLATFORM_VERSION}" && \ sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" && \ sdkmanager "${ANDROID_SYSTEM_IMAGE}" && \ echo | avdmanager create avd -n android-${ANDROID_PLATFORM_VERSION} -k "${ANDROID_SYSTEM_IMAGE}" RUN chown -R vscode:vscode ${ANDROID_HOME} && \ chown -R vscode:vscode ${ANDROID_USER_HOME} RUN mkdir -p ${FLUTTER_HOME} && \ wget "https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz" \ -O flutter.tar.xz && \ tar xf flutter.tar.xz -C ${FLUTTER_HOME} --strip-components=1 && \ rm flutter.tar.xz && \ chown -R vscode:vscode ${FLUTTER_HOME} USER vscode RUN dart --disable-analytics && flutter --disable-analytics ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Flutter", "dockerFile": "Dockerfile", "remoteUser": "vscode", "updateRemoteUserUID": true, "runArgs": ["--device=/dev/dri", "--device=/dev/kvm", "--group-add=video"], "features": { "ghcr.io/devcontainers-extra/features/shellcheck:1": {}, "ghcr.io/devcontainers-extra/features/shfmt:1": {}, "ghcr.io/prulloac/devcontainer-features/pre-commit:1": {} }, "postCreateCommand": "flutter clean && flutter pub get", "customizations": { "vscode": { "extensions": [ "Dart-Code.dart-code", "Dart-Code.flutter", "esbenp.prettier-vscode", "mkhl.shfmt", "redhat.vscode-yaml" ], "settings": { "[dart]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "explicit" }, "editor.selectionHighlight": false, "editor.tabCompletion": "onlySnippets", "editor.wordBasedSuggestions": "off" }, "[shellscript]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.shellcheck": "explicit" } }, "shfmt.executableArgs": ["-i", "2", "-s", "-bn", "-ci", "-sr"], "shellcheck.customArgs": ["-o", "all"], "shellcheck.enable": true, "shellcheck.enableQuickFix": true, "shellcheck.exclude": [], "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml", "editor.formatOnSave": true } } } }, "containerEnv": { "DISPLAY": "${localEnv:DISPLAY}", "XDG_RUNTIME_DIR": "/tmp/vscode-wayland", "WAYLAND_DISPLAY": "${localEnv:WAYLAND_DISPLAY}", "GDK_BACKEND": "wayland", "QT_QPA_PLATFORM": "xcb", "QT_X11_NO_MITSHM": "1" }, "mounts": [ "source=${localEnv:XDG_RUNTIME_DIR},target=/tmp/vscode-wayland,type=bind,consistency=cached", "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,consistency=cached" ] } ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pub" directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yaml ================================================ name: CI on: push: branches: - main pull_request: branches: - main jobs: test: name: CI runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install sqlite run: sudo apt-get update && sudo apt-get -y install sqlite3 libsqlite3-dev - name: Setup flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.24.5' channel: 'stable' cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: Flutter version run: flutter --version - name: Install dependencies run: flutter pub get - name: Check licenses run: | dart pub global activate very_good_cli dart pub global run very_good_cli:very_good packages check licenses \ --dependency-type='direct-main,transitive' \ --allowed='MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0,Zlib' - name: Analyze code (lint) run: flutter analyze --fatal-infos --fatal-warnings - name: Analyze code (format) run: dart format --set-exit-if-changed . - name: Run test run: flutter test --coverage - name: Check test coverage uses: VeryGoodOpenSource/very_good_coverage@v2 with: path: '${{ github.workspace }}/coverage/lcov.info' min_coverage: 60 ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' env: PROPERTIES_PATH: "./android/key.properties" jobs: build: name: Build runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@v4 - name: Install sqlite run: sudo apt-get update && sudo apt-get -y install sqlite3 libsqlite3-dev - name: Setup java uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: '21' - name: Setup signing environment run: | echo keyPassword=\${{ secrets.APP_SIGN_KEY_PASSWORD }} > ${{ env.PROPERTIES_PATH }} echo storePassword=\${{ secrets.APP_SIGN_STORE_PASSWORD }} >> ${{ env.PROPERTIES_PATH }} echo keyAlias=\${{ secrets.APP_SIGN_KEY_ALIAS }} >> ${{ env.PROPERTIES_PATH }} echo storeFile=key.jks >> ${{ env.PROPERTIES_PATH }} echo "${{ secrets.APP_SIGN_KEY_JKS }}" | base64 --decode > android/app/key.jks - name: Setup flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.24.5' channel: 'stable' cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: Flutter version run: flutter --version - name: Install dependencies run: flutter pub get - name: Check licenses run: | dart pub global activate very_good_cli dart pub global run very_good_cli:very_good packages check licenses \ --dependency-type='direct-main,transitive' \ --allowed='MIT,BSD-3-Clause,BSD-2-Clause,Apache-2.0,Zlib' - name: Analyze code (lint) run: flutter analyze --fatal-infos --fatal-warnings - name: Analyze code (format) run: dart format --set-exit-if-changed . - name: Run test run: flutter test --coverage - name: Check test coverage uses: VeryGoodOpenSource/very_good_coverage@v2 with: path: '${{ github.workspace }}/coverage/lcov.info' exclude: '**/*_observer.dart' min_coverage: 60 - name: Build appbundle run: flutter build appbundle --release - name: Build apk run: flutter build apk --release - name: Build apk per ABI run: flutter build apk --split-per-abi - name: Release uses: ncipollo/release-action@v1 with: owner: ${{ github.repository_owner }} body: 'See [CHANGELOG](https://github.com/tmaegel/ntodotxt/blob/main/CHANGELOG.md)' artifacts: > build/app/outputs/bundle/release/app-release.aab, build/app/outputs/flutter-apk/app-release.apk, build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk, build/app/outputs/flutter-apk/app-arm64-v8a-release.apk, build/app/outputs/flutter-apk/app-x86_64-release.apk token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.lock *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .classpath .project .settings/ # Flutter repo-specific /bin/cache/ /bin/internal/bootstrap.bat /bin/internal/bootstrap.sh /bin/mingit/ /dev/benchmarks/mega_gallery/ /dev/bots/.recipe_deps /dev/bots/android_tools/ /dev/devicelab/ABresults*.json /dev/docs/doc/ /dev/docs/flutter.docs.zip /dev/docs/lib/ /dev/docs/pubspec.yaml /dev/integration_tests/**/xcuserdata /dev/integration_tests/**/Pods /packages/flutter/coverage/ version analysis_benchmark.json # packages file containing multi-root paths .packages.generated # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies **/generated_plugin_registrant.dart .packages .pub-preload-cache/ .pub/ build/ flutter_*.png linked_*.ds unlinked.ds unlinked_spec.ds # Android related .android/ **/android/**/gradle-wrapper.jar .gradle/ **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java **/android/key.properties *.jks # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/.last_build_id **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/ephemeral **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # macOS **/Flutter/ephemeral/ **/Pods/ **/macos/Flutter/GeneratedPluginRegistrant.swift **/macos/Flutter/ephemeral **/xcuserdata/ # Windows **/windows/flutter/generated_plugin_registrant.cc **/windows/flutter/generated_plugin_registrant.h **/windows/flutter/generated_plugins.cmake # Linux **/linux/flutter/generated_plugin_registrant.cc **/linux/flutter/generated_plugin_registrant.h **/linux/flutter/generated_plugins.cmake # Coverage coverage/ # Symbols app.*.symbols # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock !.vscode/settings.json docker-compose.yml .scannerwork/ # devenv .devenv* devenv.local.nix # direnv .direnv # nix !flake.lock local.properties ================================================ FILE: .gitmodules ================================================ [submodule ".flutter"] path = .flutter url = https://github.com/flutter/flutter ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "db7ef5bf9f59442b0e200a90587e8fa5e0c6336a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: android create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: ios create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: linux create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: macos create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: web create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a - platform: windows create_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a base_revision: db7ef5bf9f59442b0e200a90587e8fa5e0c6336a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: .pre-commit-config.yaml ================================================ exclude: (^android/|^build/|^coverage/|^ios/|^linux/|^macos/|^web/|^windows/) fail_fast: true default_install_hook_types: [pre-commit] default_stages: [pre-commit] repos: - repo: "https://github.com/pre-commit/pre-commit-hooks" rev: "v4.5.0" hooks: - id: check-added-large-files args: ['--maxkb=1024'] - id: check-case-conflict - id: pretty-format-json args: ["--autofix", "--indent=4", "--no-sort-keys"] - id: check-merge-conflict - id: check-symlinks - id: check-yaml - id: check-json - id: check-toml - id: check-xml - id: destroyed-symlinks - id: detect-private-key - id: trailing-whitespace - id: double-quote-string-fixer - repo: local hooks: - id: dart format name: dart format language: system entry: dart format --set-exit-if-changed files: \.dart$ - id: flutter analyze name: flutter analyze language: system entry: flutter analyze --fatal-infos --fatal-warnings files: \.dart$ - id: flutter test name: flutter test language: system entry: flutter test --coverage pass_filenames: false - id: test coverage name: test coverage language: system entry: dart run test_cov_console -l pass_filenames: false verbose: true ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "flutter: run (Linux)", "type": "shell", "command": "flutter", "args": ["run", "-d", "linux"], "group": "build", "problemMatcher": [] }, { "label": "flutter: pub get", "type": "shell", "command": "flutter", "args": ["pub", "get"], "group": "build", "problemMatcher": [] }, { "label": "flutter: clean", "type": "shell", "command": "flutter", "args": ["clean"], "group": "build", "problemMatcher": [] } ] } ================================================ FILE: .yamlfmt ================================================ formatter: type: basic include_document_start: false retain_line_breaks_single: true trim_trailing_whitespace: true pad_line_comments: 2 ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.17.0] - 2026-03-13 ### Added - Option to enable or disable swipe-right and swipe-left actions for list items #107 - Option to delete filters via swipe-right action ## [0.16.1] - 2026-01-05 ### Fixed - 204 responses indicate no errors during the ping ## [0.16.0] - 2025-09-26 ### Added - Confirmation dialog when deleting a task with a swipe gesture #102 ## [0.15.0] - 2025-09-12 ### Added - Mark todos as done/undone via swipe gesture #42 #80 - Delete todos via swipe gesture ## [0.14.2] - 2025-07-17 ### Fixed - Page rebuild when navigating between filter pages #97 ## [0.14.1] - 2025-07-07 ### Fixed - Editing the filter name - No longer push main routes to the navigator stack ### Added - Add FloatingActionButton for easy marking of todos as done/undone #95 ## [0.14.0] - 2025-06-22 ### Added - Donation link ### Changed - Allow to choose completion date #83 ### Fixed - Displays initially an empty search result list if no search term has been entered - Don't allow due dates in the past and completion dates in the future ## [0.13.1] - 2025-06-10 ### Changed - Improvement of the database handling - No longer prevent landscape mode #88 ### Removed - Deprecated code for backwards compatibility ## [0.13.0] - 2025-05-22 ### Changed - Bump flutter version to 3.24.5 - Bump version of a bunch of dependencies - Sets default localization to 'en' for date picker #90 ## [0.12.4] - 2025-03-29 ### Fixed - Recognize whether a file already exists on the server side ### Changed - Improvement of path handling ## [0.12.3] - 2025-03-15 ### Fixed - Migrate app data from cache directory to data directory #78 #84 ## [0.12.2] - 2025-01-06 ### Fixed - Remove duplicate slash in file path #67, #79 ### Added - Add filter/todo save button to app bar ## [0.12.1] - 2024-12-11 ### Fixed - Deselecting priority ## [0.12.0] - 2024-12-02 ### Added - Checkbox to accept untrusted SSL certificates #72 ### Changed - Removes the save button and adds a save/discard dialog instead when leaving the todo/filter page #61 ### Fixed - Tag dialogs don't show all possible tags ## [0.11.0] - 2024-11-01 ## Added - Possibility to edit the due date without resetting it first #52 - Sort filters alphabetically #50 - Possibility to show and hide the password in the password field ### Changed - Auto apply changes in priority, project, context and key-value dialogs #65 - Auto apply projects and contexts tags if a new todo is created within the filter page #44 - Context and project tags will no longer change to lower case #64 ## [0.10.1] - 2024-09-22 ### Changed - Bump flutter version to 3.19.6 ### Fixed - First word not capitalized #63 - Allow single character key-value pairs #53 ## [0.10.0] - 2024-06-09 ## Added - Possibility to configure the remote path and local/remote filename #56 ## Removed - BREAKING CHANGE: The username is no longer automatically appended to webdav base url (reinitialize your app if needed) ## [0.9.1] - 2024-05-23 ### Fixed - No auto-space is inserted after selecting a word from suggestion #36 ## [0.9.0] - 2024-05-03 ### Added - Adds the full range of priorities from A to Z #48 ### Fixed - Removes id from the the todo key values #34 ## [0.8.1] - 2024-04-25 ### Fixed - Fixes an issue that the file picker was not opened for android api versions lower than 28 #45 ## [0.8.0] - 2024-04-01 ### Added - Custome file name of the local todo file while initialization of the app #35 ### Changed - File name and path can no longer be changed after initializing the app #35 - Update splash screen ## [0.7.1] - 2024-03-26 ### Changed - Adjusts configuration of textfield suggestions #36 ## [0.7.0] - 2024-03-20 ### Added - Add an intro screen #31 - Highlights filter chip in a different color if filter has updated - Tags can now also occur inline of a todo on the list view - Long todos are displayed shortened on the list view ### Changed - Update login screen #31 - Refactor initial loading and login routines - Add hint if no tags are available on tag dialog - Improve error handling and the resulting messages ### Fixed - Trim whitespaces of filter name before updating - Fix issue of todo textfield if todo is very long - Fix small style issues ## [0.6.2] - 2024-03-13 ### Fixed - Requests folder permission on the initial setup screen #30 - Base url may also ends with the username #28 - Updates default filter directly if it has been changed in the settings - Sorts todos by description only and completed todos come always at last - Resets settings correctly if logout ## [0.6.1] - 2024-03-05 ### Added - Hide keyboard if tap outside of textfield ### Changed - Bump file_picker to 6.2.0 - Bump flutter_bloc to 8.1.4 - Bump go_router to 13.2.0 - Bump sqflite_common_ffi to 2.3.2+1 - Bump sqlite3_flutter_libs to 0.5.20 - Bump url_launcher to 6.2.5 - Update style of drawer - Update style of loading spinner ### Fixed - Filter todo list on search page correctly - Order todos for the different filters/groupings correctly - Keep scroll position of todo list if todo was created or edited - Solve error while initialization on desktop ## [0.6.0] - 2024-02-28 ### Added - Add new widget tests and refactor existing ones ### Changed - Disable landscape mode - Add a confirmation dialog when the app settings are reset - Improve the appearance of the todo list page - Make app bar transparent - Hide floating action button if keyboard is open - Hide floating action button (save) if todo or filter has not be changed - Hide floating action button (save) if name todo or filter is empty ### Removed - Remove the functionality to set the todo completion state by swiping ### Fixed - Improved error handling on login screen - Improved text field behavior when creating or editing todos #27 - Prevention of + and @ characters at the beginning of the tag when displayed in the tag dialog ## [0.5.1] - 2024-02-21 ### Added - Hide primary floating action button when scrolling down and show 'go to top' button instead ### Changed - Remove bottom bar - Transparent bottom system navigation bar and edge to edge view - Small style adjustments of the snackbar and loading indicator - Replace app launcher icon ### Fixed - Dismiss dialogs on back button - Resolve some build warnings - Resolve some minor theme issues ## [0.5.0] - 2024-02-16 ### Added - Add possibility to customize the local path of the todo.txt file #7 - Tests the connection to the webdav before login ### Changed - Improve the appearance of the login screen ### Fixed - Activate the previous item in the drawer when navigating back - Ignore empty lines in todo.txt file ## [0.4.7] - 2024-02-06 ### Fixed - Add missing permission android.permission.INTERNET #20 ## [0.4.6] - 2024-02-04 ### Fixed - Pin tag/version of flutter submodule to v3.16.9 ## [0.4.5] - 2024-02-03 ### Changed - Bump flutter version to 3.16.9 - Update some dialogs ### Fixed - Server port for the webdav connection is optional #12 #17 - Sometimes the hamburger menu gets lost #18 - dense attribute is not neccessary for material3 themes ## [0.4.4] - 2024-01-16 ### Changed - Move drawer to appbar (mobile only) - Redesign todo and filter detail page/view ### Fixed - Disable allowBackup in AndroidManifest.xml - Some dialogs are scrollable if the keyboad appears - Fix regex for hostname validation #8 ## [0.4.3] - 2024-01-08 ### Changed - Sign apks ### Fixed - Add missing `flutter_launcher_icons` dependency ## [0.4.2] - 2024-01-05 ### Added - Add `flutter` as git submodule ## [0.4.1] - 2024-01-05 ### Added - Add metadata (`fastlane`) to get the app ready for deployment in the fdroid store ### Fixed - Add version code to `pubspec.yaml` ## [0.4.0] - 2024-01-04 ### Added - App icon (made by @colebemis) - Confirmation dialog for deleting todo or filter ### Changed - Update drawer (mobile) style - Disable 'Apply' button in dialogs if unnecessary (e.g. empty list) - Bump `flutter` to 3.16.5 - Bump `go_router` to 13.0.1 - Bump `url_launcher` to 6.2.2 ### Removed - Remove `google_fonts` ## [0.3.0] - 2023-12-22 ### Added - Add functionality to save and manage filters - Add database (`sqflite`) and controller to persist data (filter and settings) - Add simple loading / splash screen while initialize the app ### Changed - Save default filter settings in sqlite database instead of shared preferences - Theme and UI improvements and some redesign (app bar, dialogs, ...) - Replace navigation drawer with bottom sheet (for mobile) and navigation rail (desktop) ### Removed - Remove dependencie shared_preferences - Remove todo selection functionality ### Fixed - Add error state to FilterState and handle/show errors ## [0.2.0] - 2023-12-11 ### Added - Add swipe (left/right) action to toggle the completion of todo ### Changed - Minor style adjustments to the theme and layout ### Fixed - Hide tags (projects, contexts, key values) in tag dialog if already present in todo - Toggle filter/order/group by if tapping on the label - Notification bars are floating ## [0.1.0] - 2023-12-08 ### Added - Intiial release [unreleased]: https://github.com/tmaegel/ntodotxt/compare/v0.17.0...HEAD [0.16.1]: https://github.com/tmaegel/ntodotxt/compare/v0.16.1...v0.17.0 [0.16.1]: https://github.com/tmaegel/ntodotxt/compare/v0.16.0...v0.16.1 [0.16.0]: https://github.com/tmaegel/ntodotxt/compare/v0.15.0...v0.16.0 [0.15.0]: https://github.com/tmaegel/ntodotxt/compare/v0.14.2...v0.15.0 [0.14.2]: https://github.com/tmaegel/ntodotxt/compare/v0.14.1...v0.14.2 [0.14.1]: https://github.com/tmaegel/ntodotxt/compare/v0.14.0...v0.14.1 [0.14.0]: https://github.com/tmaegel/ntodotxt/compare/v0.13.1...v0.14.0 [0.13.1]: https://github.com/tmaegel/ntodotxt/compare/v0.13.0...v0.13.1 [0.13.0]: https://github.com/tmaegel/ntodotxt/compare/v0.12.4...v0.13.0 [0.12.4]: https://github.com/tmaegel/ntodotxt/compare/v0.12.3...v0.12.4 [0.12.3]: https://github.com/tmaegel/ntodotxt/compare/v0.12.2...v0.12.3 [0.12.2]: https://github.com/tmaegel/ntodotxt/compare/v0.12.1...v0.12.2 [0.12.1]: https://github.com/tmaegel/ntodotxt/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/tmaegel/ntodotxt/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/tmaegel/ntodotxt/compare/v0.10.1...v0.11.0 [0.10.1]: https://github.com/tmaegel/ntodotxt/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/tmaegel/ntodotxt/compare/v0.9.1...v0.10.0 [0.9.1]: https://github.com/tmaegel/ntodotxt/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/tmaegel/ntodotxt/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/tmaegel/ntodotxt/compare/v0.8.0...v0.8.1 [0.8.0]: https://github.com/tmaegel/ntodotxt/compare/v0.7.1...v0.8.0 [0.7.1]: https://github.com/tmaegel/ntodotxt/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/tmaegel/ntodotxt/compare/v0.6.2...v0.7.0 [0.6.2]: https://github.com/tmaegel/ntodotxt/compare/v0.6.1...v0.6.2 [0.6.1]: https://github.com/tmaegel/ntodotxt/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/tmaegel/ntodotxt/compare/v0.5.1...v0.6.0 [0.5.1]: https://github.com/tmaegel/ntodotxt/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/tmaegel/ntodotxt/compare/v0.4.7...v0.5.0 [0.4.7]: https://github.com/tmaegel/ntodotxt/compare/v0.4.6...v0.4.7 [0.4.6]: https://github.com/tmaegel/ntodotxt/compare/v0.4.5...v0.4.6 [0.4.5]: https://github.com/tmaegel/ntodotxt/compare/v0.4.4...v0.4.5 [0.4.4]: https://github.com/tmaegel/ntodotxt/compare/v0.4.3...v0.4.4 [0.4.3]: https://github.com/tmaegel/ntodotxt/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/tmaegel/ntodotxt/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/tmaegel/ntodotxt/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/tmaegel/ntodotxt/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/tmaegel/ntodotxt/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/tmaegel/ntodotxt/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/tmaegel/ntodotxt/releases/tag/v0.1.0 ================================================ FILE: Caddyfile ================================================ { debug } localhost { uri strip_prefix /nc reverse_proxy nextcloud:80 } 10.0.2.2 { tls internal uri strip_prefix /nc reverse_proxy nextcloud:80 } ================================================ FILE: Dockerfile_fdroid ================================================ FROM registry.gitlab.com/fdroid/docker-executable-fdroidserver:master RUN apt-get update && apt-get install -y \ openjdk-17-jdk-headless \ && rm -rf /var/lib/apt/lists/* \ && update-java-alternatives -a ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Toni Mägel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: screenshots APP_ID = "de.tnmgl.ntodotxt" FDROID_REPO = "${HOME}/Downloads/nosync/fdroiddata" sonarqube: docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 127.0.0.1:9000:9000 sonarqube:10.4-community scan: docker run \ --rm --name sonar-scanner-cli \ -e SONAR_HOST_URL="http://sonarqube:9000" \ -e SONAR_SCANNER_OPTS="-Dsonar.projectKey=ntodotxt" \ -e SONAR_TOKEN="token" \ -v "$(shell pwd):/usr/src" \ --link sonarqube \ sonarsource/sonar-scanner-cli licenses: flutter pub run flutter_oss_licenses:generate.dart icon: flutter pub run flutter_launcher_icons integration_env_configure: docker exec -it -u www-data nextcloud_local php occ config:system:set trusted_domains 2 --value=10.0.2.2 screenshots: flutter emulators --launch "Pixel_7_API_34_extension_level_7_x86_64" && sleep 10 flutter drive \ --driver=test_driver/screenshot_integration_test.dart \ --target=integration_test/screenshot_integration_test.dart || true adb emu kill preview_screenshots: flutter emulators --launch "Pixel_7_API_34_extension_level_7_x86_64" && sleep 10 flutter drive \ --driver=test_driver/screenshot_integration_test.dart \ --target=integration_test/preview_app_integration_test.dart || true adb emu kill fdroid_lint: cd $(FDROID_REPO) && docker run \ --rm \ --name fdroid \ -v "${HOME}/.android-sdk/":/opt/android-sdk \ -v $(FDROID_REPO):/repo \ -e ANDROID_HOME:/opt/android-sdk \ registry.gitlab.com/fdroid/docker-executable-fdroidserver:master lint -v $(APP_ID) fdroid_signature: apksigner verify --print-certs app.apk | grep SHA-256 cd $(FDROID_REPO) && docker run \ --rm \ --name fdroid \ -v "${HOME}/.android-sdk/":/opt/android-sdk \ -v $(FDROID_REPO):/repo \ -e ANDROID_HOME:/opt/android-sdk \ registry.gitlab.com/fdroid/docker-executable-fdroidserver:master signatures unsigned/app.apk fdroid_build: docker build -t fdroidserver -f Dockerfile_fdroid . fdroid_run: cd $(FDROID_REPO) && docker run \ --rm \ --name fdroid \ -v "${HOME}/.android-sdk/":/opt/android-sdk \ -v $(FDROID_REPO):/repo \ -e ANDROID_HOME:/opt/android-sdk \ fdroidserver build -v -l $(APP_ID) ================================================ FILE: README.md ================================================ # ntodotxt [![CI](https://github.com/tmaegel/ntodotxt/actions/workflows/ci.yaml/badge.svg)](https://github.com/tmaegel/ntodotxt/actions/workflows/ci.yaml) [![Release](https://img.shields.io/github/v/release/tmaegel/ntodotxt)](https://github.com/tmaegel/ntodotxt/releases) [![F-Droid](https://img.shields.io/f-droid/v/de.tnmgl.ntodotxt.svg?logo=F-Droid)](https://f-droid.org/packages/de.tnmgl.ntodotxt) [![License](https://img.shields.io/badge/License-MIT-yellow)](https://opensource.org/licenses/MIT) [![Flutter](https://img.shields.io/badge/_Flutter_-3.24.5-grey.svg?&logo=Flutter&logoColor=white&labelColor=blue)](https://github.com/flutter/flutter) With `ntodotxt` you can manage your todos in a [todo.txt](https://github.com/todotxt/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. This application is under active development and will continue to be modified and improved over time. ## Screenshots ## Downloads [Get it on F-Droid](https://f-droid.org/packages/de.tnmgl.ntodotxt/) ## Features ### v1.0 - [x] Manage todos in [todo.txt](https://github.com/todotxt/todo.txt) format - [x] Manage todos locally and/or synchronize todos via webdav with a server of your choice - [x] Custom path and filename of todo files (local and remote) - [x] Search todos - [x] Create custom views of todos via filters - [ ] Sort (ascending/descending) todos by criteria such as priority, creation date or due date - [ ] Android widget - [ ] Import/Export existing todos from/to file - [ ] Import/Export filters and other settings - [ ] Language localization (e.g. english, german) - [ ] [Recurring](https://c306.net/t/topydo-docs/#Recurrence) tasks - [ ] Archiving of completed todos (done.txt) - [ ] ... ### Low priority - [ ] Build and publish to Google Play (Android) - [ ] Build and publish as `flatpak` to [flathub](https://flathub.org/) (Linux) - [ ] Build and publish as `snap` to [snapcraft](https://snapcraft.io/) (Linux) - [ ] Build and publish to Microsoft Store (Windows) ## Build ### General [Flutter SDK](https://docs.flutter.dev/get-started/install) is required to build this project. ### Building on Linux 1. First you need to get the source code of `ntodotxt`. ```bash git clone https://github.com/tmaegel/ntodotxt ``` 2. 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). ```bash dnf install sqlite-devel libsecret-devel1 ``` 3. Open project via [Android Studio](https://developer.android.com/studio). 4. Click the `Run` button and it will be built and run automatically. 5. Or you can build and run from command line. ```bash flutter pub get flutter run # or flutter build ``` If an error occurs during the build process, please follow these [steps](https://docs.flutter.dev/get-started/install/linux/desktop#development-tools). ## Sponsorship `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. Buy Me a Coffee at ko-fi.com Thanks for all the love and support. ## Alternatives There 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. ================================================ FILE: analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at https://dart.dev/lints. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: prefer_single_quotes: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } android { namespace = "de.tnmgl.ntodotxt" compileSdk = 35 ndkVersion = "25.1.8937393" compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } kotlinOptions { jvmTarget = JavaVersion.VERSION_21 } defaultConfig { applicationId = "de.tnmgl.ntodotxt" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release } } // Removes DependencyInfoBlock for F-Droid. dependenciesInfo { // Disables dependency metadata when building APKs. includeInApk = false // Disables dependency metadata when building Android App Bundles. includeInBundle = false } } flutter { source = "../.." } dependencies {} ext.abiCodes = ["x86_64": 1, "armeabi-v7a": 2, "arm64-v8a": 3] import com.android.build.OutputFile android.applicationVariants.all { variant -> variant.outputs.each { output -> def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) if (abiVersionCode != null) { output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode } } } ================================================ FILE: android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/de/tnmgl/ntodotxt/MainActivity.kt ================================================ package de.tnmgl.ntodotxt import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath }() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.3.2" apply false id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" ================================================ FILE: docker-compose.yaml ================================================ services: nextcloud: image: nextcloud:27 container_name: nextcloud expose: - 80 environment: NEXTCLOUD_TRUSTED_DOMAINS: "127.0.0.1,10.0.2.2" OVERWRITEHOST: "localhost:8443" OVERWRITEPROTOCOL: "https" volumes: - nextcloud:/var/www/html networks: - caddy caddy: image: caddy:latest container_name: reverse_proxy ports: - "127.0.0.1:8080:80" - "127.0.0.1:8443:443" - "127.0.0.1:8443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:z networks: - caddy volumes: nextcloud: caddy: networks: caddy: ================================================ FILE: emulatorctl ================================================ #!/usr/bin/env bash function show_usage() { echo "Usage: $(basename "$") [options]" echo "" echo "Options: -l List emulators / avds -c [name] Create emulator / avd -d Delete emulator / avd -r Run emulator / avd -e Exit emulator / avd -v [api] Anroid API -h Display this message" } if [[ $# -eq 0 ]]; then show_usage exit 1 fi ACTION=0 ANDROID_API=36 EMULATOR_NAME="" while getopts ":hldrec:v:" arg; do case "${arg}" in l) avdmanager list avd exit 0 ;; d) avdmanager delete avd --name "$(avdmanager list avd -c | fzf)" exit 0 ;; r) echo "Starting emulator ..." flutter emulators --launch "$(avdmanager list avd -c | fzf)" exit 0 ;; e) echo "Exiting emulator ..." adb emu kill exit 0 ;; c) ACTION="create" EMULATOR_NAME="${OPTARG}" ;; v) ANDROID_API="${OPTARG}" ;; h | *) show_usage exit 0 ;; esac done if [[ ${ACTION} == "create" ]]; then avdmanager create avd --name "${EMULATOR_NAME}-${ANDROID_API}" --package "system-images;android-${ANDROID_API};google_apis;x86_64" fi exit 0 ================================================ FILE: fonts/LICENSE ================================================ Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: integration_test/login/login_integration_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:ntodotxt/intro/page/intro_page.dart'; import 'package:ntodotxt/login/page/login_page.dart'; import 'package:ntodotxt/main.dart'; import 'package:path_provider/path_provider.dart'; void main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown], ); final String appDataDir = '${(await getApplicationDocumentsDirectory()).path}/'; group('login', () { group('initial', () { testWidgets('screen is visible', (tester) async { await tester.pumpWidget( App(appDataDir: appDataDir), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); expect(find.byType(IntroPage), findsOneWidget); }); }); group('local', () { testWidgets('default local path', (tester) async { await tester.pumpWidget( App(appDataDir: appDataDir), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); expect(find.byType(IntroPage), findsOneWidget); await tester.tap(find.byTooltip('Next page')); await tester.pumpAndSettle(); await tester.tap(find.text('Use local mode')); await tester.pumpAndSettle(); expect(find.byType(LocalLoginView), findsOneWidget); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && widget.title is Text && (widget.title as Text).data == 'Local path' && widget.subtitle != null && (widget.subtitle as Text).data == appDataDir, ), findsOneWidget, ); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.byType(App), findsOneWidget); await tester.tap(find.byTooltip('Open drawer')); await tester.pumpAndSettle(); await tester.drag( find.byType(DraggableScrollableSheet), const Offset(0, -500)); await tester.pumpAndSettle(); await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); await tester.scrollUntilVisible(find.text('Reinitialization'), 500); await tester.tap(find.text('Reinitialization')); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(AlertDialog), matching: find.text('Reninitialize'), ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); expect(find.byType(IntroPage), findsOneWidget); }); }); group('webdav', () { testWidgets('default local path', (tester) async { await tester.pumpWidget( App(appDataDir: appDataDir), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); expect(find.byType(IntroPage), findsOneWidget); await tester.tap(find.byTooltip('Next page')); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Next page')); await tester.pumpAndSettle(); await tester.tap(find.text('Use webdav mode')); await tester.pumpAndSettle(); expect(find.byType(WebDAVLoginView), findsOneWidget); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && widget.title is Text && (widget.title as Text).data == 'Local path' && widget.subtitle != null && (widget.subtitle as Text).data == appDataDir, ), findsOneWidget, ); await tester.enterText( find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ), 'https://10.0.2.2:8443', ); await tester.tap(find.byType(Checkbox)); await tester.enterText( find.ancestor( of: find.text('Path'), matching: find.byType(TextFormField), ), '/remote.php/dav/files/test', ); await tester.enterText( find.ancestor( of: find.text('Username'), matching: find.byType(TextFormField), ), 'test', ); await tester.enterText( find.ancestor( of: find.text('Password'), matching: find.byType(TextFormField), ), 'test', ); await tester.pumpAndSettle(); await tester.tap(find.byType(AppBar)); await tester.pumpAndSettle(); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.byType(App), findsOneWidget); await tester.tap(find.byTooltip('Open drawer')); await tester.pumpAndSettle(); await tester.drag( find.byType(DraggableScrollableSheet), const Offset(0, -500)); await tester.pumpAndSettle(); await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); await tester.scrollUntilVisible(find.text('Reinitialization'), 500); await tester.tap(find.text('Reinitialization')); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(AlertDialog), matching: find.text('Reninitialize'), ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); expect(find.byType(IntroPage), findsOneWidget); }); }); }); } ================================================ FILE: integration_test/preview_app_integration_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; class AppPreview extends StatelessWidget { final String image; final String message; final Color foregroundColor = Colors.white; final Color backgroundColor = Colors.lightBlue[100]!; final Color deviceFrameColor = Colors.blueGrey; AppPreview({ required this.image, required this.message, super.key, }); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( backgroundColor: backgroundColor, body: Container( padding: const EdgeInsets.only(bottom: 24.0), child: Column( children: [ Flexible( flex: 1, child: Center( child: Text( message, style: Theme.of(context).textTheme.titleLarge, ), ), ), Flexible( flex: 6, child: Stack( alignment: Alignment.topCenter, children: [ Container( decoration: BoxDecoration( color: deviceFrameColor, border: Border.all( width: 12, color: deviceFrameColor, ), borderRadius: BorderRadius.circular(34), ), child: Stack( alignment: Alignment.topLeft, children: [ Stack( alignment: Alignment.topRight, children: [ ClipRRect( borderRadius: BorderRadius.circular(24), child: Image.network( image, fit: BoxFit.cover, ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 8.0), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.signal_wifi_4_bar, color: foregroundColor, size: Theme.of(context) .textTheme .bodySmall ?.fontSize, ), Icon( Icons.signal_cellular_4_bar, color: foregroundColor, size: Theme.of(context) .textTheme .bodySmall ?.fontSize, ), const SizedBox(width: 2.0), Icon( Icons.battery_full, color: foregroundColor, size: Theme.of(context) .textTheme .bodySmall ?.fontSize, ), ], ), ), ], ), Padding( padding: const EdgeInsets.symmetric( horizontal: 12.0, vertical: 8.0), child: Text( '12:00', style: TextStyle( fontSize: Theme.of(context) .textTheme .bodySmall ?.fontSize, color: foregroundColor, ), ), ), ], ), ), ], ), ), ], ), ), ), ); } } void main() async { const String repoUrl = 'https://raw.githubusercontent.com/tmaegel/ntodotxt/HEAD/'; final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized(); // Hide android status bar. SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); group('dark mode', () { group('create app store preview screenshots', () { testWidgets('of todo list (default)', (tester) async { await tester.pumpWidget( AppPreview( message: 'Compact overview of all todos', image: '$repoUrl/screenshots/phone/1.png', ), ); // Ensure the image is loaded/displayed. await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('preview/1'); }); testWidgets('of todo list (with open drawer)', (tester) async { await tester.pumpWidget( AppPreview( message: 'Custom filters', image: '$repoUrl/screenshots/phone/2.png', ), ); // Ensure the image is loaded/displayed. await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('preview/2'); }); testWidgets('of todo edit page', (tester) async { await tester.pumpWidget( AppPreview( message: 'Edit and create todos', image: '$repoUrl/screenshots/phone/3.png', ), ); // Ensure the image is loaded/displayed. await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('preview/3'); }); testWidgets('of filter list (default)', (tester) async { await tester.pumpWidget( AppPreview( message: 'Compact overview of all filters', image: '$repoUrl/screenshots/phone/4.png', ), ); // Ensure the image is loaded/displayed. await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('preview/4'); }); testWidgets('of filter edit page', (tester) async { await tester.pumpWidget( AppPreview( message: 'Edit and create filters', image: '$repoUrl/screenshots/phone/5.png', ), ); // Ensure the image is loaded/displayed. await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('preview/5'); }); }); }); } ================================================ FILE: integration_test/screenshot_integration_test.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart' show FilterController; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter, ListFilter, ListGroup, ListOrder; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/login/state/login_state.dart' show LoginLocal, LoginState, LoginWebDAV; import 'package:ntodotxt/main.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; // https://developer.android.com/studio/run/emulator-networking#networkaddresses // Special alias to your host loopback interface (127.0.0.1 on your development machine) const String server = 'https://10.0.2.2:8443'; const String path = '/remote.php/dav/files'; const String username = 'test'; const String password = 'test'; class FakeController extends Fake implements FilterController { List items = [ const Filter( id: 1, name: 'Agenda', order: ListOrder.ascending, filter: ListFilter.incompletedOnly, group: ListGroup.upcoming, ), const Filter( id: 2, name: 'Highly prioritized', order: ListOrder.ascending, filter: ListFilter.incompletedOnly, group: ListGroup.project, priorities: {Priority.A}, ), const Filter( id: 3, name: 'Projectideas', order: ListOrder.ascending, filter: ListFilter.incompletedOnly, group: ListGroup.none, projects: {'projectideas'}, ), const Filter( id: 4, name: 'Completed only', order: ListOrder.ascending, filter: ListFilter.completedOnly, group: ListGroup.none, ), ]; @override Future> list() async { return Future.value(items); } } class AppTester extends StatelessWidget { final DatabaseController dbController = const DatabaseController(inMemoryDatabasePath); final ThemeMode? themeMode; final String appCacheDir; const AppTester({ this.themeMode, required this.appCacheDir, super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FakeController(), ), ), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => LoginCubit( state: const LoginWebDAV( server: server, path: path, username: username, password: password, acceptUntrustedCert: true, ), ), ), BlocProvider( create: (BuildContext context) => TodoFileCubit( repository: context.read(), localPath: appCacheDir, )..load(), ), BlocProvider( create: (BuildContext context) => DrawerCubit(), ), // Default filter BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), )..load(), ), BlocProvider( create: (BuildContext context) => FilterListBloc( repository: context.read(), ) ..add(const FilterListSubscriped()) ..add(const FilterListSynchronizationRequested()), ), ], child: Builder( builder: (BuildContext context) { return BlocBuilder( builder: (BuildContext context, LoginState state) { if (state is LoginLocal || state is LoginWebDAV) { return CoreApp(loginState: state); } else { return const InitialApp(); } }, ); }, ), ), ); } } void main() async { final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); final DateTime today = DateTime.now(); final List todoList = [ Todo( creationDate: today.subtract(const Duration(days: 7)), priority: Priority.A, description: 'Automate the generation of +app screenshots +learnflutter @development @automation @productivity due:${Todo.date2Str(today.add(const Duration(days: 3)))!}', ), Todo( creationDate: today.subtract(const Duration(days: 14)), priority: Priority.B, description: 'Publish this +app +learnflutter @development due:${Todo.date2Str(today.add(const Duration(days: 7)))!}', ), Todo( creationDate: today.subtract(const Duration(days: 2)), description: 'Increase test +coverage for this +app +learnflutter @development @testing @productivity', ), Todo( creationDate: today.subtract(const Duration(days: 2)), completion: true, completionDate: today.subtract(const Duration(days: 1)), description: 'Write some tests for this +app +learnflutter @development @testing @productivity', ), Todo( creationDate: today.subtract(const Duration(days: 21)), priority: Priority.C, description: 'Setup a good project management tool @development @productivity', ) ]; setUp(() async { // Setup todos. WebDAVClient client = WebDAVClient( server: server, path: path, username: username, password: password, ); try { await client.upload( content: todoList.join(Platform.lineTerminator), filename: 'todo.txt'); } catch (e) { fail('An exception was thrown: $e'); } }); group('dark mode', () { group('take screenshots', () { testWidgets('of todo list (default)', (tester) async { await tester.pumpWidget( AppTester( themeMode: ThemeMode.dark, appCacheDir: (await getApplicationCacheDirectory()).path, ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('phone/1'); }); testWidgets('of todo list (with open drawer)', (tester) async { await tester.pumpWidget( AppTester( themeMode: ThemeMode.dark, appCacheDir: (await getApplicationCacheDirectory()).path, ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await tester.tap(find.byTooltip('Open drawer')); await tester.pumpAndSettle(); await tester.drag( find.byType(DraggableScrollableSheet), const Offset(0, -500)); await tester.pumpAndSettle(); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('phone/2'); }); testWidgets('of todo edit page', (tester) async { await tester.pumpWidget( AppTester( themeMode: ThemeMode.dark, appCacheDir: (await getApplicationCacheDirectory()).path, ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await tester.tap(find.text( 'Publish this +app +learnflutter @development', findRichText: true, )); await tester.pumpAndSettle(); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('phone/3'); }); testWidgets('of filter list (default)', (tester) async { await tester.pumpWidget( AppTester( themeMode: ThemeMode.dark, appCacheDir: (await getApplicationCacheDirectory()).path, ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await tester.tap(find.byTooltip('Open drawer')); await tester.pumpAndSettle(); await tester.drag( find.byType(DraggableScrollableSheet), const Offset(0, -500)); await tester.pumpAndSettle(); await tester.tap(find.text('Filters')); await tester.pumpAndSettle(); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('phone/4'); }); testWidgets('of filter edit page', (tester) async { await tester.pumpWidget( AppTester( themeMode: ThemeMode.dark, appCacheDir: (await getApplicationCacheDirectory()).path, ), ); await tester.pumpAndSettle(const Duration(milliseconds: 5000)); await tester.tap(find.byTooltip('Open drawer')); await tester.pumpAndSettle(); await tester.drag( find.byType(DraggableScrollableSheet), const Offset(0, -500)); await tester.pumpAndSettle(); await tester.tap(find.text('Filters')); await tester.pumpAndSettle(); await tester.tap(find.text('Projectideas')); await tester.pumpAndSettle(); await binding.convertFlutterSurfaceToImage(); await tester.pumpAndSettle(); await binding.takeScreenshot('phone/5'); }); }); }); } ================================================ FILE: integration_test/webdav/client/webdav_client_test.dart ================================================ import 'dart:io' show Platform; import 'dart:math'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; import 'package:webdav_client/webdav_client.dart'; const String scheme = 'https'; const int port = 8443; final String host = Platform.isAndroid ? '10.0.2.2' : 'localhost'; WebDAVClient createWebDAVClient({ String? server, String? path, String? username, String? password, }) { return WebDAVClient( server: server ?? '$scheme://$host:$port', path: path ?? '/remote.php/dav/files/test', username: username ?? 'test', password: password ?? 'test', acceptUntrustedCert: true, ); } String randomString({int len = 8}) { final Random r = Random(); const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; return List.generate(len, (index) => chars[r.nextInt(chars.length)]).join(); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('WebDAVClient', () { group('ping()', () { test('correct connection', () async { final WebDAVClient client = createWebDAVClient(); try { await client.ping(); } catch (e) { fail('An exception was thrown: $e'); } }); test('wrong host', () async { final WebDAVClient client = createWebDAVClient(server: '$scheme://webdav:$port'); expectLater( () async => await client.ping(), throwsA( isA(), ), ); }); test('wrong port', () async { final WebDAVClient client = createWebDAVClient(server: '$scheme://$host:9999'); expectLater( () async => await client.ping(), throwsA( isA(), ), ); }); }); group('listFiles()', () { test('list', () async { final WebDAVClient client = createWebDAVClient(); try { await client.listFiles(path: '/'); } catch (e) { fail('An exception was thrown: $e'); } }); }); group('file create() & fileExists()', () { test('file does exist (1)', () async { final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.create(filename); expectLater( await client.fileExists(filename: filename), true, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('file does exist (2)', () async { final String directory = randomString(); final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.mkdir(directory: directory); await client.create('$directory/$filename'); expectLater( await client.fileExists(filename: '$directory/$filename'), true, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('file doesn\'t exist (1)', () async { final WebDAVClient client = createWebDAVClient(); try { expectLater( await client.fileExists(filename: 'abc.xyz'), false, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('file doesn\'t exist (2)', () async { final String directory = randomString(); final WebDAVClient client = createWebDAVClient(); try { await client.mkdir(directory: directory); expectLater( await client.fileExists(filename: '/$directory/abc.xyz'), false, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('directory/file doesn\'t exist in this path', () async { final String directory = randomString(); final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.mkdir(directory: directory); await client.create(filename); expectLater( await client.fileExists(filename: '$directory/$filename'), false, ); } catch (e) { fail('An exception was thrown: $e'); } }); }); group('upload()', () { test('file upload', () async { final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.upload(content: 'abc', filename: filename); expectLater(await client.fileExists(filename: filename), true); } catch (e) { fail('An exception was thrown: $e'); } }); test('file upload within already existing directory', () async { final String directory = randomString(); final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.mkdir(directory: directory); await client.upload(content: 'abc', filename: '$directory/$filename'); expectLater( await client.fileExists(filename: '$directory/$filename'), true, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('file upload within non existing directory', () async { final String directory = randomString(); final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.upload(content: 'abc', filename: '$directory/$filename'); expectLater( await client.fileExists(filename: '$directory/$filename'), true, ); } catch (e) { fail('An exception was thrown: $e'); } }); test('exception', () async { final WebDAVClient client = createWebDAVClient(password: 'wrong'); expectLater( () async => await client.upload(content: 'abc', filename: 'todo.txt'), throwsA( isA(), ), ); }); }); group('download()', () { test('file download', () async { final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.upload(content: 'abc', filename: filename); expectLater(await client.download(filename: filename), 'abc'); } catch (e) { fail('An exception was thrown: $e'); } }); test('file download within directory', () async { final String directory = randomString(); final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.upload(content: 'abc', filename: '$directory/$filename'); expectLater( await client.download(filename: '$directory/$filename'), 'abc', ); } catch (e) { fail('An exception was thrown: $e'); } }); test('exception', () async { final WebDAVClient client = createWebDAVClient(password: 'wrong'); expectLater( () async => await client.download(filename: 'todo.txt'), throwsA( isA(), ), ); }); }); group('getFile()', () { test('valid', () async { final String filename = '${randomString()}.txt'; final WebDAVClient client = createWebDAVClient(); try { await client.upload(content: 'abc', filename: filename); File f = await client.getFile(filename: filename); expect(f.mTime is DateTime, true); } catch (e) { fail('An exception was thrown: $e'); } }); test('exception', () async { final WebDAVClient client = createWebDAVClient(); expectLater( () async => await client.getFile(filename: 'unknown.txt'), throwsA( isA(), ), ); }); }); }); } ================================================ FILE: ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import Flutter import UIKit @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You 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. ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Ntodotxt CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ntodotxt CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: lib/adaptive_layout/widget/adaptive_layout.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/misc.dart'; import 'package:ntodotxt/drawer/widget/drawer.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_state.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/login/state/login_state.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; class AdaptiveLayout extends StatelessWidget { final Widget child; const AdaptiveLayout({ required this.child, super.key, }); @override Widget build(BuildContext context) { // if (MediaQuery.of(context).size.width < maxScreenWidthCompact) { // @todo: Activate WideLayout later! return NotificationWrapper( child: NarrowLayout(child: child), ); // } else { // return NotificationWrapper( // child: WideLayout(child: child), // ); // } } } class NotificationWrapper extends StatelessWidget { final Widget child; const NotificationWrapper({ required this.child, super.key, }); @override Widget build(BuildContext context) { return MultiBlocListener( listeners: [ BlocListener( listener: (BuildContext context, LoginState state) { if (state is LoginError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, TodoFileState state) { if (state is TodoFileError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, FilterListState state) { if (state is FilterListError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, FilterState state) { if (state is FilterError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, TodoListState state) { if (state is TodoListError) { SnackBarHandler.error(context, state.message); } }, ), ], child: child, ); } } class NarrowLayout extends StatelessWidget { final Widget child; const NarrowLayout({ required this.child, super.key, }); @override Widget build(BuildContext context) => child; } class WideLayout extends StatelessWidget { final Widget child; const WideLayout({ required this.child, super.key, }); @override Widget build(BuildContext context) { return Row( children: [ const NavigationRailDrawer(), const VerticalDivider(width: 2), Expanded(child: child), ], ); } } ================================================ FILE: lib/app_info/page/app_details_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/constants/app.dart'; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:url_launcher/url_launcher.dart'; class AppInfoPage extends StatelessWidget { const AppInfoPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( appBar: MainAppBar(title: 'About'), body: AppInfoView(), ); } } class AppInfoView extends StatelessWidget { static const String repoUrl = 'https://github.com/tmaegel/ntodotxt'; const AppInfoView({super.key}); @override Widget build(BuildContext context) { return ListView( padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [ ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.update), title: const Text('Version'), subtitle: const Text('v$version'), onTap: () => _openUrl('$repoUrl/blob/main/CHANGELOG.md'), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.code), title: const Text('Source code'), subtitle: const Text(repoUrl), onTap: () => _openUrl(repoUrl), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.bug_report_outlined), title: const Text('Issue tracker'), subtitle: const Text('$repoUrl/issues'), onTap: () => _openUrl('$repoUrl/issues'), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.email_outlined), title: const Text('Contact me'), subtitle: const Text('tnmgl@posteo.de'), onTap: () => _openUrl('mailto:tnmgl@posteo.de'), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.volunteer_activism), title: const Text('Donation'), subtitle: const Text('Support this app'), onTap: () => _openUrl('https://ko-fi.com/tnmgl'), ), ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: const Icon(Icons.shield_outlined), title: const Text('Licence'), subtitle: const Text('MIT License'), onTap: () => context.pushNamed('licenses'), ), ], ); } Future _openUrl(String urlStr) async { final Uri url = Uri.parse(urlStr); if (!await launchUrl(url)) { throw Exception('Could not open $urlStr'); } } } ================================================ FILE: lib/common/bloc_observer.dart ================================================ // coverage:ignore-file import 'package:flutter_bloc/flutter_bloc.dart' show Bloc, BlocBase, BlocObserver, Change, Transition; import 'package:ntodotxt/main.dart' show log; class GenericBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); log.fine( 'STATE CHANGE: ${change.currentState.runtimeType} > ${change.nextState.runtimeType}'); log.finer('${bloc.runtimeType} $change'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); log.finest('${bloc.runtimeType} $transition'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { log.fine('${bloc.runtimeType} $error $stackTrace'); super.onError(bloc, error, stackTrace); } } ================================================ FILE: lib/common/constants/app.dart ================================================ // coverage:ignore-file const String version = '0.17.0'; /// https://m3.material.io/foundations/layout/applying-layout/window-size-classes const int maxScreenWidthCompact = 600; const String defaultLocalTodoPath = '/'; const String defaultRemoteTodoPath = '/'; const String defaultTodoFilename = 'todo.txt'; const String defaultDoneFilename = 'done.txt'; ================================================ FILE: lib/common/exception/exceptions.dart ================================================ // coverage:ignore-file sealed class TodoException implements Exception { final String message; const TodoException(this.message); @override String toString() => message; } class TodoNotFound extends TodoException { final String? id; const TodoNotFound({ this.id, }) : super('Todo with id $id could not be found'); } class TodoMissingId extends TodoException { const TodoMissingId() : super('Todo has no id key'); } class TodoStringMalformed extends TodoException { final String str; const TodoStringMalformed({ required this.str, }) : super('Todo string is malformed: "$str"'); } class TodoInvalidProjectTag extends TodoException { final String tag; const TodoInvalidProjectTag({ required this.tag, }) : super('Invalid project tag: $tag'); } class TodoInvalidContextTag extends TodoException { final String tag; const TodoInvalidContextTag({ required this.tag, }) : super('Invalid context tag: $tag'); } class TodoInvalidKeyValueTag extends TodoException { final String tag; const TodoInvalidKeyValueTag({ required this.tag, }) : super('Invalid key value tag: $tag'); } class TodoForbiddenCompletionDate extends TodoException { const TodoForbiddenCompletionDate() : super('Completion date is forbidden if todo is incompleted'); } class TodoMissingCompletionDate extends TodoException { const TodoMissingCompletionDate() : super('Completed todo requires a completion date'); } ================================================ FILE: lib/common/misc.dart ================================================ // coverage:ignore-file import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; class PlatformInfo { static bool get isDesktopOS { return Platform.isMacOS || Platform.isLinux || Platform.isWindows; } static bool get isAppOS { return Platform.isIOS || Platform.isAndroid; } } enum MessageType { success, info, error } class SnackBarHandler { static void _call(BuildContext context, MessageType type, String message) { Color backgroundColor = Theme.of(context).colorScheme.primaryContainer; Color foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer; switch (type) { case MessageType.success: backgroundColor = Theme.of(context).colorScheme.primaryContainer; foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer; break; case MessageType.info: backgroundColor = Theme.of(context).colorScheme.primaryContainer; foregroundColor = Theme.of(context).colorScheme.onPrimaryContainer; break; case MessageType.error: backgroundColor = Theme.of(context).colorScheme.error; foregroundColor = Theme.of(context).colorScheme.onError; break; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( backgroundColor: backgroundColor, duration: type == MessageType.error ? const Duration(seconds: 10) : const Duration(seconds: 4), content: Text( message, style: TextStyle(color: foregroundColor), ), ), ); } static void success(BuildContext context, String message) => _call(context, MessageType.success, message); static void info(BuildContext context, String message) => _call(context, MessageType.info, message); static void error(BuildContext context, String message) => _call(context, MessageType.error, message); } class CustomScrollBehavior extends MaterialScrollBehavior { @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.stylus, PointerDeviceKind.unknown, }; } class Debouncer { Timer? _timer; final int milliseconds; Debouncer({required this.milliseconds}); void run(VoidCallback action) { _timer?.cancel(); _timer = Timer(Duration(milliseconds: milliseconds), action); } void dispose() { _timer?.cancel(); _timer = null; } } class PopScopeDrawer extends StatelessWidget { final Widget child; const PopScopeDrawer({ required this.child, super.key, }); @override Widget build(BuildContext context) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, T? result) { if (didPop) { return; } context.read().back(); Navigator.of(context).pop(); }, child: child, ); } } ================================================ FILE: lib/common/router/router.dart ================================================ // coverage:ignore-file import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/adaptive_layout/widget/adaptive_layout.dart'; import 'package:ntodotxt/app_info/page/app_details_page.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/page/filter_create_edit_page.dart'; import 'package:ntodotxt/filter/page/filter_list_page.dart'; import 'package:ntodotxt/licenses/page/licenses_page.dart'; import 'package:ntodotxt/setting/page/settings_page.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/page/todo_create_edit_page.dart'; import 'package:ntodotxt/todo/page/todo_list_page.dart'; import 'package:ntodotxt/todo/page/todo_search_page.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; class AppRouter { final GlobalKey _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _shellNavigatorKey = GlobalKey(debugLabel: 'shell'); AppRouter(); late final GoRouter config = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/todo', debugLogDiagnostics: false, routes: [ ShellRoute( navigatorKey: _shellNavigatorKey, builder: (context, state, child) { return AdaptiveLayout(child: child); }, routes: [ GoRoute( path: '/settings', name: 'settings', builder: (BuildContext context, GoRouterState state) { return const SettingsPage(); }, routes: [ GoRoute( path: 'settings/app-info', name: 'app-info', builder: (BuildContext context, GoRouterState state) { return const AppInfoPage(); }, routes: [ GoRoute( path: 'settings/app-info/licenses', name: 'licenses', builder: (BuildContext context, GoRouterState state) { return const LicenceListPage(); }, ), ], ), ], ), GoRoute( path: '/todo', name: 'todo-list', builder: (BuildContext context, GoRouterState state) { Filter? filter = state.extra as Filter?; return TodoListPage( filter: filter, key: ValueKey(filter?.id ?? 'default'), ); }, routes: [ GoRoute( path: 'todo/create', name: 'todo-create', builder: (BuildContext context, GoRouterState state) { Todo todo = state.extra as Todo; return TodoCreateEditPage( initTodo: todo, newTodo: true, projects: context.read().state.projects, contexts: context.read().state.contexts, keyValues: context.read().state.keyValues, ); }, ), GoRoute( path: 'todo/edit', name: 'todo-edit', builder: (BuildContext context, GoRouterState state) { Todo todo = state.extra as Todo; return TodoCreateEditPage( initTodo: todo, newTodo: false, projects: context.read().state.projects, contexts: context.read().state.contexts, keyValues: context.read().state.keyValues, ); }, ), GoRoute( path: 'todo/search', name: 'todo-search', builder: (BuildContext context, GoRouterState state) { Filter? filter = state.extra as Filter?; return TodoSearchPage(filter: filter); }, ), ], ), GoRoute( path: '/filter', name: 'filter-list', builder: (BuildContext context, GoRouterState state) { return const FilterListPage(); }, routes: [ GoRoute( path: 'filter/create', name: 'filter-create', builder: (BuildContext context, GoRouterState state) { return FilterCreateEditPage( projects: context.read().state.projects, contexts: context.read().state.contexts, ); }, ), GoRoute( path: 'filter/edit', name: 'filter-edit', builder: (BuildContext context, GoRouterState state) { Filter filter = state.extra as Filter; return FilterCreateEditPage( initFilter: filter, projects: context.read().state.projects, contexts: context.read().state.contexts, ); }, ), ], ), ], ), ], ); } ================================================ FILE: lib/common/theme/theme.dart ================================================ // coverage:ignore-file import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:ntodotxt/common/misc.dart'; final ThemeData light = CustomTheme.light; final ThemeData dark = CustomTheme.dark; /// Customize versions of the theme data. final ThemeData lightTheme = light.copyWith( appBarTheme: light.appBarTheme.copyWith( backgroundColor: Colors.transparent, systemOverlayStyle: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarBrightness: Brightness.dark, statusBarIconBrightness: Brightness.dark, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: Brightness.dark, ), ), snackBarTheme: light.snackBarTheme.copyWith( elevation: 0.0, ), splashColor: PlatformInfo.isAppOS ? Colors.transparent : null, chipTheme: light.chipTheme.copyWith(), expansionTileTheme: light.expansionTileTheme.copyWith( shape: const Border(), collapsedBackgroundColor: light.appBarTheme.backgroundColor, textColor: light.colorScheme.primary, ), listTileTheme: light.listTileTheme.copyWith( selectedColor: light.textTheme.bodySmall?.color, selectedTileColor: light.hoverColor, contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), scrollbarTheme: light.scrollbarTheme.copyWith( thickness: WidgetStateProperty.all(5.0), ), bottomAppBarTheme: light.bottomAppBarTheme.copyWith(), floatingActionButtonTheme: light.floatingActionButtonTheme.copyWith( elevation: 0.0, focusElevation: 0.0, hoverElevation: 0.0, ), inputDecorationTheme: light.inputDecorationTheme.copyWith( filled: false, isDense: true, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), progressIndicatorTheme: light.progressIndicatorTheme.copyWith( color: light.colorScheme.primary, circularTrackColor: light.colorScheme.primaryContainer, refreshBackgroundColor: light.colorScheme.primaryContainer, ), ); final ThemeData darkTheme = dark.copyWith( appBarTheme: dark.appBarTheme.copyWith( backgroundColor: Colors.transparent, systemOverlayStyle: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarBrightness: Brightness.light, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: Brightness.light, ), ), snackBarTheme: dark.snackBarTheme.copyWith( elevation: 0.0, ), splashColor: PlatformInfo.isAppOS ? Colors.transparent : null, chipTheme: dark.chipTheme.copyWith(), expansionTileTheme: dark.expansionTileTheme.copyWith( shape: const Border(), collapsedBackgroundColor: dark.appBarTheme.backgroundColor, textColor: dark.colorScheme.primary, ), listTileTheme: dark.listTileTheme.copyWith( selectedColor: dark.textTheme.bodySmall?.color, selectedTileColor: dark.hoverColor, contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ), scrollbarTheme: dark.scrollbarTheme.copyWith( thickness: WidgetStateProperty.all(5.0), ), bottomAppBarTheme: dark.bottomAppBarTheme.copyWith(), floatingActionButtonTheme: dark.floatingActionButtonTheme.copyWith( elevation: 0.0, focusElevation: 0.0, hoverElevation: 0.0, ), inputDecorationTheme: dark.inputDecorationTheme.copyWith( filled: false, isDense: true, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), progressIndicatorTheme: dark.progressIndicatorTheme.copyWith( color: dark.colorScheme.primary, circularTrackColor: dark.colorScheme.primaryContainer, refreshBackgroundColor: dark.colorScheme.primaryContainer, ), ); // Theme config for FlexColorScheme version 7.3.x. Make sure you use // same or higher package version, but still same major version. If you // use a lower package version, some properties may not be supported. // In that case remove them after copying this theme to your app. class CustomTheme { static ThemeData get light { return FlexThemeData.light( scheme: FlexScheme.bahamaBlue, surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, blendLevel: 7, subThemesData: const FlexSubThemesData( blendOnLevel: 10, blendOnColors: false, useM2StyleDividerInM3: true, alignedDropdown: true, useInputDecoratorThemeInDialogs: true, ), visualDensity: FlexColorScheme.comfortablePlatformDensity, useMaterial3: true, swapLegacyOnMaterial3: true, fontFamily: 'OpenSans', ); } static ThemeData get dark { return FlexThemeData.dark( scheme: FlexScheme.bahamaBlue, surfaceMode: FlexSurfaceMode.levelSurfacesLowScaffold, blendLevel: 13, subThemesData: const FlexSubThemesData( blendOnLevel: 20, useM2StyleDividerInM3: true, alignedDropdown: true, useInputDecoratorThemeInDialogs: true, ), visualDensity: FlexColorScheme.comfortablePlatformDensity, useMaterial3: true, swapLegacyOnMaterial3: true, fontFamily: 'OpenSans', ); } } ================================================ FILE: lib/common/widget/app_bar.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/misc.dart' show CustomScrollBehavior; import 'package:ntodotxt/drawer/widget/drawer.dart'; import 'package:ntodotxt/filter/widget/filter_chip.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; class MainAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; final Widget? toolbar; final Widget? bottom; const MainAppBar({ required this.title, this.toolbar, this.bottom, super.key, }); @override Widget build(BuildContext context) { // @todo: Activate WideLayout later! // final bool narrowView = // MediaQuery.of(context).size.width < maxScreenWidthCompact; return AppBar( // titleSpacing: narrowView ? 0.0 : null, titleSpacing: 0.0, title: Text(title), // leading: narrowView && Scaffold.of(context).hasDrawer leading: Scaffold.of(context).hasDrawer ? Builder( builder: (BuildContext context) { return IconButton( tooltip: 'Open drawer', icon: const Icon(Icons.menu), onPressed: () async { await showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => const BottomSheetNavigationDrawer(), ); }, ); }, ) : null, actions: toolbar == null ? null : [ toolbar!, const SizedBox(width: 8), ], bottom: bottom == null ? null : PreferredSize( preferredSize: Size.zero, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: bottom!, ), ), ); } // Scaffold requires as appbar a class that implements PreferredSizeWidget. @override Size get preferredSize => Size.fromHeight(bottom == null ? kToolbarHeight : 110); } class AppBarFilterList extends StatelessWidget { const AppBarFilterList({super.key}); @override Widget build(BuildContext context) { final ScrollController controller = ScrollController(); return BlocBuilder( builder: (BuildContext context, TodoListState todoListState) { return ScrollConfiguration( behavior: CustomScrollBehavior(), child: SingleChildScrollView( controller: controller, scrollDirection: Axis.horizontal, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const FilterOrderChip(), const SizedBox(width: 4), const FilterFilterChip(), const SizedBox(width: 4), const FilterGroupChip(), const SizedBox(width: 4), const FilterPrioritiesChip(), const SizedBox(width: 4), FilterProjectsChip(availableTags: todoListState.projects), const SizedBox(width: 4), FilterContextsChip(availableTags: todoListState.contexts), ], ), ), ), ); }, ); } } ================================================ FILE: lib/common/widget/chip.dart ================================================ import 'package:flutter/material.dart'; class BasicIconChip extends StatelessWidget { final String label; final IconData iconData; final bool mono; const BasicIconChip({ required this.label, required this.iconData, this.mono = false, super.key, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 0.0), decoration: BoxDecoration( color: mono ? Theme.of(context).colorScheme.surfaceContainerHigh : Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(iconData, size: 14.0), const SizedBox(width: 2.0), Text( label, style: TextStyle( color: mono ? Theme.of(context).colorScheme.onSurfaceVariant : Theme.of(context).colorScheme.onSecondaryContainer, ), ) ], ), ); } } class BasicChip extends StatelessWidget { final String label; final bool mono; const BasicChip({ required this.label, this.mono = false, super.key, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 0.0), decoration: BoxDecoration( color: mono ? Theme.of(context).colorScheme.surfaceContainerHigh : Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(4), ), child: Text( label, style: TextStyle( color: mono ? Theme.of(context).colorScheme.onSurfaceVariant : Theme.of(context).colorScheme.onSecondaryContainer, ), ), ); } } class GenericActionChip extends StatelessWidget { final Widget label; final Widget avatar; final Function() onPressed; final bool selected; const GenericActionChip({ required this.label, required this.avatar, required this.onPressed, this.selected = false, super.key, }); @override Widget build(BuildContext context) { return ActionChip( avatar: avatar, label: label, padding: EdgeInsets.zero, side: selected == true ? BorderSide(color: Theme.of(context).colorScheme.primary) : null, labelPadding: const EdgeInsets.only(right: 8.0), onPressed: () async => onPressed(), ); } } class GenericChoiceChip extends StatelessWidget { final Widget label; final bool selected; final bool showCheckmark; final Function(bool selected) onSelected; const GenericChoiceChip({ required this.label, this.selected = false, this.showCheckmark = false, required this.onSelected, super.key, }); @override Widget build(BuildContext context) { return ChoiceChip( label: label, selected: selected, showCheckmark: showCheckmark, // Workaround: https://github.com/flutter/flutter/issues/67797 visualDensity: const VisualDensity( horizontal: -4.0, vertical: -4.0, ), onSelected: (bool selected) => onSelected(selected), ); } } class GenericChipGroup extends StatelessWidget { final List children; final WrapAlignment alignment; const GenericChipGroup({ required this.children, this.alignment = WrapAlignment.start, super.key, }); @override Widget build(BuildContext context) { return Wrap( spacing: 4.0, // gap between adjacent chips alignment: alignment, runSpacing: 4.0, // gap between lines runAlignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.start, children: children, ); } } ================================================ FILE: lib/common/widget/confirm_dialog.dart ================================================ import 'package:flutter/material.dart'; class ConfirmationDialog extends StatelessWidget { final String title; final String message; final String actionLabel; final String cancelLabel; const ConfirmationDialog({ required this.title, required this.message, required this.actionLabel, required this.cancelLabel, super.key, }); static Future dialog({ required BuildContext context, required String title, required String message, required String actionLabel, required String cancelLabel, }) async { bool? result = await showDialog( useRootNavigator: false, context: context, barrierDismissible: false, // User must tap button. builder: (BuildContext context) => ConfirmationDialog( title: title, message: message, actionLabel: actionLabel, cancelLabel: cancelLabel, ), ); return result ?? false; } @override Widget build(BuildContext context) { return AlertDialog( title: Text(title), content: Text(message), actions: [ TextButton( child: Text(cancelLabel), onPressed: () => Navigator.pop(context, false), ), TextButton( child: Text(actionLabel), onPressed: () => Navigator.pop(context, true), ), ], ); } } ================================================ FILE: lib/common/widget/contexts_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/widget/tag_dialog.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; import 'package:ntodotxt/todo/state/todo_cubit.dart'; class FilterContextTagDialog extends TagDialog { final FilterCubit cubit; const FilterContextTagDialog({ required this.cubit, super.title = 'Contexts', super.tagName = 'context', super.availableTags, super.addTags = false, super.key = const Key('FilterContextTagDialog'), }); @override RegExp get regex => RegExp(r'^\S+$'); static Future dialog({ required BuildContext context, required FilterCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => FilterContextTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _FilterContextTagDialogState(); } class _FilterContextTagDialogState extends TagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (String t) => Tag( name: t, selected: widget.cubit.state.filter.contexts.contains(t), ), ), }; } @override void onUpdate() { widget.cubit.updateContexts({ for (Tag t in tags) if (t.selected) t.name }); } } class TodoContextTagDialog extends TagDialog { final TodoCubit cubit; const TodoContextTagDialog({ required this.cubit, super.title = 'Contexts', super.tagName = 'context', super.availableTags, super.addTags = true, super.key = const Key('TodoContextTagDialog'), }); @override RegExp get regex => RegExp(r'^\S+$'); static Future dialog({ required BuildContext context, required TodoCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => TodoContextTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _TodoContextTagDialogState(); } class _TodoContextTagDialogState extends TagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (String t) => Tag( name: t, selected: widget.cubit.state.todo.contexts.contains(t), ), ), // Overwrites contexts of todo with selected=true ...widget.cubit.state.todo.contexts.map( (String t) => Tag(name: t, selected: true), ), }; } @override void onUpdate() { widget.cubit.updateContexts({ for (Tag t in tags) if (t.selected) t.name }); } } ================================================ FILE: lib/common/widget/date_picker.dart ================================================ import 'package:flutter/material.dart'; class TodoDatePicker { static final int defaultDaysOffset = 3650; const TodoDatePicker(); static Future pickDate({ required BuildContext context, required DateTime? initialDate, int? startDateDaysOffset, int? endDateDaysOffset, }) async { final DateTime initial = initialDate ?? DateTime.now(); return await showDatePicker( useRootNavigator: false, context: context, firstDate: initial.subtract( Duration(days: startDateDaysOffset ?? defaultDaysOffset), ), initialDate: initial, lastDate: initial.add( Duration(days: endDateDaysOffset ?? defaultDaysOffset), ), locale: const Locale('en', 'GB'), ); } } ================================================ FILE: lib/common/widget/filter_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show ListFilter; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; class FilterStateFilterDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const FilterStateFilterDialog({ required this.cubit, super.key, }) : items = const { 'All': ListFilter.all, 'Completed only': ListFilter.completedOnly, 'Incompleted only': ListFilter.incompletedOnly, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => FilterStateFilterDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListFilter value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, title: Text(key), value: value, groupValue: cubit.state.filter.filter, onChanged: (ListFilter? value) { if (value != null) { cubit.updateFilter(value); } Navigator.pop(context); }, ); }, ), ); } } class DefaultFilterStateFilterDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const DefaultFilterStateFilterDialog({ required this.cubit, super.key, }) : items = const { 'All': ListFilter.all, 'Completed only': ListFilter.completedOnly, 'Incompleted only': ListFilter.incompletedOnly, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => DefaultFilterStateFilterDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListFilter value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, value: value, title: Text(key), groupValue: cubit.state.filter.filter, onChanged: (ListFilter? value) { if (value != null) { cubit.updateDefaultFilter(value); } Navigator.pop(context); }, ); }, ), ); } } ================================================ FILE: lib/common/widget/group_by_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show ListGroup; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; class FilterStateGroupDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const FilterStateGroupDialog({ required this.cubit, super.key, }) : items = const { 'None': ListGroup.none, 'Upcoming': ListGroup.upcoming, 'Priority': ListGroup.priority, 'Project': ListGroup.project, 'Context': ListGroup.context, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => FilterStateGroupDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListGroup value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, title: Text(key), value: value, groupValue: cubit.state.filter.group, onChanged: (ListGroup? value) { if (value != null) { cubit.updateGroup(value); } Navigator.pop(context); }, ); }, ), ); } } class DefaultFilterStateGroupDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const DefaultFilterStateGroupDialog({ required this.cubit, super.key, }) : items = const { 'None': ListGroup.none, 'Upcoming': ListGroup.upcoming, 'Priority': ListGroup.priority, 'Project': ListGroup.project, 'Context': ListGroup.context, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => DefaultFilterStateGroupDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListGroup value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, value: value, title: Text(key), groupValue: cubit.state.filter.group, onChanged: (ListGroup? value) { if (value != null) { cubit.updateDefaultGroup(value); } Navigator.pop(context); }); }, ), ); } } ================================================ FILE: lib/common/widget/info_dialog.dart ================================================ import 'package:flutter/material.dart'; class InfoDialog extends StatelessWidget { final String title; final String message; const InfoDialog({ required this.title, required this.message, super.key, }); static Future dialog({ required BuildContext context, required String title, required String message, }) async { return await showDialog( useRootNavigator: false, context: context, barrierDismissible: true, builder: (BuildContext context) => InfoDialog( title: title, message: message, ), ); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(title), titleTextStyle: Theme.of(context).textTheme.titleMedium, content: Text(message), ); } } ================================================ FILE: lib/common/widget/input_dialog.dart ================================================ import 'package:flutter/material.dart'; class InputDialog extends StatelessWidget { final String title; final String label; final String? value; const InputDialog({ required this.title, required this.label, this.value, super.key, }); static Future dialog({ required BuildContext context, required String title, required String label, String? value, }) async { return await showDialog( useRootNavigator: false, context: context, barrierDismissible: false, // User must tap button. builder: (BuildContext context) => InputDialog(title: title, label: label, value: value), ); } @override Widget build(BuildContext context) { final TextEditingController controller = TextEditingController(); controller.text = value ?? ''; return AlertDialog( title: Text(title), content: TextField( controller: controller, decoration: InputDecoration(hintText: label), ), actions: [ TextButton( child: const Text('Cancel'), onPressed: () => Navigator.pop(context), ), TextButton( child: const Text('Ok'), onPressed: () => Navigator.pop(context, controller.text), ), ], ); } } ================================================ FILE: lib/common/widget/key_values_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/widget/tag_dialog.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/state/todo_cubit.dart'; class TodoKeyValueTagDialog extends TagDialog { final TodoCubit cubit; const TodoKeyValueTagDialog({ required this.cubit, super.title = 'Key values', super.tagName = 'key:value', super.availableTags, super.addTags = true, super.key = const Key('TodoKeyValueTagDialog'), }); @override RegExp get regex => Todo.patternKeyValue; static Future dialog({ required BuildContext context, required TodoCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => TodoKeyValueTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _TodoKeyValueTagDialogState(); } class _TodoKeyValueTagDialogState extends TagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (String t) => Tag( name: t, selected: widget.cubit.state.todo.fmtKeyValues.contains(t), ), ), // Overwrites key values of todo with selected=true ...widget.cubit.state.todo.fmtKeyValues.map( (String t) => Tag(name: t, selected: true), ), }; } @override void onUpdate() { widget.cubit.updateKeyValues({ for (Tag t in tags) if (t.selected) t.name }); } } ================================================ FILE: lib/common/widget/order_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show ListOrder; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; class FilterStateOrderDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const FilterStateOrderDialog({ required this.cubit, super.key, }) : items = const { 'Ascending': ListOrder.ascending, 'Descending': ListOrder.descending, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => FilterStateOrderDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListOrder value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, title: Text(key), value: value, groupValue: cubit.state.filter.order, onChanged: (ListOrder? value) { if (value != null) { cubit.updateOrder(value); } Navigator.pop(context); }, ); }, ), ); } } class DefaultFilterStateOrderDialog extends StatelessWidget { final FilterCubit cubit; final Map items; const DefaultFilterStateOrderDialog({ required this.cubit, super.key, }) : items = const { 'Ascending': ListOrder.ascending, 'Descending': ListOrder.descending, }; static Future dialog({ required BuildContext context, required FilterCubit cubit, }) async { return await showDialog>( useRootNavigator: false, context: context, builder: (BuildContext context) => DefaultFilterStateOrderDialog(cubit: cubit), ); } @override Widget build(BuildContext context) { return Dialog( child: ListView.builder( shrinkWrap: true, padding: const EdgeInsets.all(16.0), itemCount: items.length, itemBuilder: (BuildContext context, int index) { String key = items.keys.elementAt(index); ListOrder value = items[key]!; return RadioListTile( key: Key('${value.name}DialogRadioButton'), contentPadding: EdgeInsets.zero, value: value, title: Text(key), groupValue: cubit.state.filter.order, onChanged: (ListOrder? value) { if (value != null) { cubit.updateDefaultOrder(value); } Navigator.pop(context); }, ); }, ), ); } } ================================================ FILE: lib/common/widget/priorities_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/misc.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; import 'package:ntodotxt/todo/state/todo_cubit.dart'; class PriorityTag { Priority priority; bool selected; PriorityTag({ required this.priority, required this.selected, }); @override String toString() => priority.name; } class PriorityTagDialog extends StatefulWidget { final String title; final Set availableTags; const PriorityTagDialog({ required this.title, this.availableTags = const {}, super.key, }); @override State createState() => PriorityTagDialogState(); } class PriorityTagDialogState extends State { // Holds the selected tags before adding to the regular state. Set tags = {}; late TextEditingController _controller; @override void initState() { super.initState(); _controller = TextEditingController(); } @override void dispose() { // Clean up the controller when the widget is disposed. _controller.dispose(); super.dispose(); } Set get sortedTags { List t = tags.toList() ..sort( (PriorityTag a, PriorityTag b) => a.toString().compareTo(b.toString()), ); return t.toSet(); } void onUpdate(PriorityTag value, bool selected) {} @override Widget build(BuildContext context) { return DraggableScrollableSheet( initialChildSize: 0.5, minChildSize: 0.15, maxChildSize: 0.9, expand: false, builder: (BuildContext context, ScrollController scrollController) { return ScrollConfiguration( behavior: CustomScrollBehavior(), child: ListView( controller: scrollController, children: [ Padding( padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), title: Text( widget.title, style: Theme.of(context).textTheme.titleLarge, ), ), ), if (tags.isNotEmpty) const Divider(), if (tags.isNotEmpty) Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), title: GenericChipGroup( children: [ for (var t in sortedTags) GenericChoiceChip( label: Text(t.priority.name), selected: t.selected, onSelected: (bool selected) => onUpdate(t, selected), ), ], ), ), ), ], ), ); }, ); } } class FilterPriorityTagDialog extends PriorityTagDialog { final FilterCubit cubit; const FilterPriorityTagDialog({ required this.cubit, super.title = 'Priorities', super.availableTags, super.key = const Key('FilterPriorityTagDialog'), }); static Future dialog({ required BuildContext context, required FilterCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => FilterPriorityTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _FilterPriorityTagDialogState(); } class _FilterPriorityTagDialogState extends PriorityTagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (Priority t) => PriorityTag( priority: t, selected: widget.cubit.state.filter.priorities.contains(t), ), ), }; } @override void onUpdate(PriorityTag value, bool selected) { setState(() { value.selected = selected; }); widget.cubit.updatePriorities({ for (PriorityTag t in tags) if (t.selected) t.priority }); } } class TodoPriorityTagDialog extends PriorityTagDialog { final TodoCubit cubit; const TodoPriorityTagDialog({ required this.cubit, super.title = 'Priorities', super.availableTags, super.key = const Key('TodoPriorityTagDialog'), }); static Future dialog({ required BuildContext context, required TodoCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => TodoPriorityTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _TodoPriorityTagDialogState(); } class _TodoPriorityTagDialogState extends PriorityTagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (Priority t) => PriorityTag( priority: t, selected: widget.cubit.state.todo.priority == t, ), ), }; } @override void onUpdate(PriorityTag value, bool selected) { setState(() { // Unset priorities first. for (PriorityTag tag in tags) { tag.selected = false; } value.selected = selected; }); if (selected) { widget.cubit.setPriority(value.priority); } else { widget.cubit.unsetPriority(); } } } ================================================ FILE: lib/common/widget/projects_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/widget/tag_dialog.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart' show FilterCubit; import 'package:ntodotxt/todo/state/todo_cubit.dart'; class FilterProjectTagDialog extends TagDialog { final FilterCubit cubit; const FilterProjectTagDialog({ required this.cubit, super.title = 'Projects', super.tagName = 'project', super.availableTags, super.addTags = false, super.key = const Key('FilterProjectTagDialog'), }); @override RegExp get regex => RegExp(r'^\S+$'); static Future dialog({ required BuildContext context, required FilterCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => FilterProjectTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _FilterProjectTagDialogState(); } class _FilterProjectTagDialogState extends TagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (String t) => Tag( name: t, selected: widget.cubit.state.filter.projects.contains(t), ), ), }; } @override void onUpdate() { widget.cubit.updateProjects({ for (Tag t in tags) if (t.selected) t.name }); } } class TodoProjectTagDialog extends TagDialog { final TodoCubit cubit; const TodoProjectTagDialog({ required this.cubit, super.title = 'Projects', super.tagName = 'project', super.availableTags, super.addTags = true, super.key = const Key('TodoProjectTagDialog'), }); @override RegExp get regex => RegExp(r'^\S+$'); static Future dialog({ required BuildContext context, required TodoCubit cubit, required Set availableTags, }) async { showModalBottomSheet( context: context, isScrollControlled: true, builder: (BuildContext context) => TodoProjectTagDialog( cubit: cubit, availableTags: availableTags, ), ); } @override State createState() => _TodoProjectTagDialogState(); } class _TodoProjectTagDialogState extends TagDialogState { @override void initState() { super.initState(); super.tags = { ...widget.availableTags.map( (String t) => Tag( name: t, selected: widget.cubit.state.todo.projects.contains(t), ), ), // Overwrites projects of todo with selected=true ...widget.cubit.state.todo.projects.map( (String t) => Tag(name: t, selected: true), ), }; } @override void onUpdate() { widget.cubit.updateProjects({ for (Tag t in tags) if (t.selected) t.name }); } } ================================================ FILE: lib/common/widget/scroll_to_top.dart ================================================ import 'package:flutter/material.dart'; class ScollToTopView extends StatefulWidget { const ScollToTopView({super.key}); @override State createState() => ScollToTopViewState(); } class ScollToTopViewState extends State { bool scrolledDown = false; late ScrollController scrollController; @override void initState() { scrollController = ScrollController() ..addListener( () { setState(() { if (scrollController.offset >= 50) { scrolledDown = true; } else { scrolledDown = false; } }); }, ); super.initState(); } @override void dispose() { scrollController.dispose(); super.dispose(); } void scrollToTop() { scrollController.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.linear); } @override Widget build(BuildContext context) { return Container(); } } ================================================ FILE: lib/common/widget/tag_dialog.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/misc.dart'; import 'package:ntodotxt/common/widget/chip.dart'; class Tag { final String name; bool selected; Tag({ required this.name, required this.selected, }); // Makes overwriting possible if 'selected' is different. @override bool operator ==(Object other) => other is Tag && name == other.name; @override int get hashCode => name.hashCode; @override String toString() => '$name ($selected)'; } class TagDialog extends StatefulWidget { final String title; final String tagName; final Set availableTags; final bool addTags; const TagDialog({ required this.title, required this.tagName, this.availableTags = const {}, this.addTags = true, super.key, }); RegExp get regex => RegExp(r'^\S+$'); @override State createState() => TagDialogState(); } class TagDialogState extends State { // Holds the selected tags before adding to the regular state. Set tags = {}; late GlobalKey _formKey; late TextEditingController _controller; @override void initState() { super.initState(); _formKey = GlobalKey(); _controller = TextEditingController(); } @override void dispose() { // Clean up the controller when the widget is disposed. _controller.dispose(); super.dispose(); } Set get sortedTags { List t = tags.toList() ..sort( (Tag a, Tag b) => a.toString().compareTo(b.toString()), ); return t.toSet(); } void onUpdate() {} @override Widget build(BuildContext context) { return DraggableScrollableSheet( initialChildSize: widget.addTags == true ? 0.9 : 0.5, minChildSize: 0.15, maxChildSize: 0.9, expand: false, builder: (BuildContext context, ScrollController scrollController) { return ScrollConfiguration( behavior: CustomScrollBehavior(), child: ListView( controller: scrollController, children: [ Padding( padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), title: Text( widget.title, style: Theme.of(context).textTheme.titleLarge, ), ), ), const Divider(), if (widget.addTags) Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), title: Form( key: _formKey, child: TextFormField( controller: _controller, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: 'Enter <${widget.tagName}> tag ...', contentPadding: EdgeInsets.zero, ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Missing tag name'; } if (!widget.regex.hasMatch(value.trim())) { return 'Invalid tag format'; } return null; }, ), ), trailing: TextButton( child: const Text('Add'), onPressed: () { if (_formKey.currentState!.validate()) { String text = _controller.text.trim(); if (text.startsWith('+') || text.startsWith('@')) { text = text.substring(1); } setState(() { tags.add(Tag(name: text, selected: true)); }); _controller.text = ''; onUpdate(); } }, ), ), ), if (widget.addTags) const Divider(), tags.isEmpty ? Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 8.0, ), title: Text('No ${widget.tagName} tags available.'), ), ) : Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), title: GenericChipGroup( children: [ for (var t in sortedTags) GenericChoiceChip( label: Text(t.name), selected: t.selected, onSelected: (bool selected) { setState(() { t.selected = selected; }); onUpdate(); }, ), ], ), ), ), ], ), ); }, ); } } ================================================ FILE: lib/database/controller/database.dart ================================================ import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/main.dart' show log; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; abstract class ModelControllerInterface { Future> list(); Future get(dynamic identifier); Future insert(T model); Future update(T model); Future delete(dynamic identifier); } class DatabaseController { static Database? _database; // Singleton pattern final String path; const DatabaseController(this.path); Future get database async { if (_database != null) { return _database!; } else { _database = await _open(); return _database!; } } Future close() async { if (_database != null) { await _database!.close(); } _database = null; } Future _open() async { // Change the default factory. On iOS/Android, if not using `sqlite_flutter_lib` // you can forget this step, it will use the sqlite version available on the system. databaseFactoryOrNull = databaseFactoryFfi; return openDatabase( path, // Set the version. This executes the onCreate function and provides a // path to perform database upgrades and downgrades. version: 1, onCreate: (Database db, int version) { log.info('Create database $path'); db.execute(Setting.tableRepr); db.execute(Filter.tableRepr); }, onUpgrade: (Database db, int oldVersion, int newVersion) { if (newVersion > oldVersion) { log.info('Perform database upgrade'); } }, onOpen: (Database db) {}, singleInstance: true, ); } } ================================================ FILE: lib/drawer/state/drawer_cubit.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/drawer/state/drawer_state.dart'; /// Keep the state of the selected item within the drawer. class DrawerCubit extends Cubit { DrawerCubit() : super(const DrawerState(index: 0)); void reset() => emit(const DrawerState(index: 0)); void next(int index) { emit( DrawerState( prevIndex: state.index, index: index, ), ); } void back() { emit( DrawerState( prevIndex: null, index: state.prevIndex ?? 0, ), ); } } ================================================ FILE: lib/drawer/state/drawer_state.dart ================================================ import 'package:equatable/equatable.dart'; final class DrawerState extends Equatable { final int index; final int? prevIndex; const DrawerState({ required this.index, this.prevIndex, }); @override List get props => [ index, prevIndex, ]; @override String toString() => 'DrawerState { index: $index prevIndex: $prevIndex }'; } ================================================ FILE: lib/drawer/widget/drawer.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/drawer/state/drawer_state.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_state.dart'; class DrawerDestination { final String label; final Widget icon; final Widget selectedIcon; final Function(BuildContext context) onTap; const DrawerDestination({ required this.label, required this.icon, required this.selectedIcon, required this.onTap, }); } class NavigationRailDrawer extends StatelessWidget { const NavigationRailDrawer({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, DrawerState drawerState) { return BlocBuilder( builder: (BuildContext context, FilterListState filterListState) { List destinations = [ DrawerDestination( label: 'Todos', icon: const Icon(Icons.checklist_outlined), selectedIcon: const Icon(Icons.checklist), onTap: (BuildContext context) => context.goNamed('todo-list'), ), DrawerDestination( label: 'Filters', icon: const Icon(Icons.filter_list_outlined), selectedIcon: const Icon(Icons.filter_list), onTap: (BuildContext context) => context.goNamed('filter-list'), ), for (Filter filter in filterListState.filterList) DrawerDestination( label: 'Filter: ${filter.name}', icon: const Icon(Icons.star_outline), selectedIcon: const Icon(Icons.star), onTap: (BuildContext context) => context.goNamed('todo-list', extra: filter), ), DrawerDestination( label: 'Settings', icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onTap: (BuildContext context) => context.goNamed('settings'), ), ]; return NavigationRail( extended: true, selectedIndex: drawerState.index, groupAlignment: -1, onDestinationSelected: (int index) { final DrawerDestination d = destinations[index]; // Navigate if location will be changed only. if (drawerState.index != index) { context.read().next(index); d.onTap(context); } }, labelType: NavigationRailLabelType.none, destinations: [ for (DrawerDestination d in destinations) NavigationRailDestination( label: Text( d.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), icon: d.icon, selectedIcon: d.selectedIcon, ), ], ); }, ); }, ); } } class BottomSheetNavigationDrawer extends StatelessWidget { const BottomSheetNavigationDrawer({super.key}); @override Widget build(BuildContext context) { return DraggableScrollableSheet( initialChildSize: 0.35, minChildSize: 0.15, maxChildSize: 0.6, expand: false, builder: (BuildContext context, ScrollController scrollController) { return BlocBuilder( builder: (BuildContext context, DrawerState drawerState) { return BlocBuilder( builder: (BuildContext context, FilterListState filterListState) { List destinations = [ DrawerDestination( label: 'Todos', icon: const Icon(Icons.checklist_outlined), selectedIcon: const Icon(Icons.checklist), onTap: (BuildContext context) => context.goNamed('todo-list'), ), DrawerDestination( label: 'Filters', icon: const Icon(Icons.filter_list_outlined), selectedIcon: const Icon(Icons.filter_list), onTap: (BuildContext context) => context.goNamed('filter-list'), ), for (Filter filter in filterListState.filterList) DrawerDestination( label: 'Filter: ${filter.name}', icon: const Icon(Icons.star_outline), selectedIcon: const Icon(Icons.star), onTap: (BuildContext context) => context.goNamed('todo-list', extra: filter), ), DrawerDestination( label: 'Settings', icon: const Icon(Icons.settings_outlined), selectedIcon: const Icon(Icons.settings), onTap: (BuildContext context) => context.pushNamed('settings'), ), ]; return ScrollConfiguration( behavior: CustomScrollBehavior(), child: ListView.builder( controller: scrollController, itemCount: destinations.length, itemBuilder: (BuildContext context, int index) { final DrawerDestination d = destinations[index]; return Column( children: [ if (index == 0) const SizedBox(height: 14.0), Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 2.0), child: ListTile( selected: drawerState.index == index, leading: drawerState.index == index ? d.selectedIcon : d.icon, title: Text( d.label, maxLines: 1, overflow: TextOverflow.ellipsis, ), shape: const StadiumBorder(), onTap: () { // Navigate if location will be changed only. if (drawerState.index != index) { context.read().next(index); d.onTap(context); } Navigator.pop(context); }, ), ), if (index == destinations.length - 2 || index == 1) const Divider(), ], ); }, ), ); }, ); }, ); }, ); } } ================================================ FILE: lib/filter/controller/fake_filter_controller.dart ================================================ // coverage:ignore-file import 'package:ntodotxt/filter/controller/filter_controller.dart' show FilterControllerInterface; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; class FakeFilterController implements FilterControllerInterface { static final List filters = []; FakeFilterController(); @override Future> list() async => filters; @override Future get(dynamic identifier) async { for (Filter s in filters) { if (s.id == identifier) { return s; } } return null; } @override Future insert(Filter model) async { filters.add(model); return filters.length; } @override Future update(Filter model) async { int index = filters.indexWhere((Filter s) => s.id == model.id); if (index != -1) { filters[index] = model; return index; } else { return 0; } } @override Future delete(dynamic identifier) async { int index = filters.indexWhere((Filter s) => s.id == identifier); if (index != -1) { filters.removeAt(index); return index; } else { return 0; } } } ================================================ FILE: lib/filter/controller/filter_controller.dart ================================================ import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; abstract class FilterControllerInterface implements ModelControllerInterface {} class FilterController implements FilterControllerInterface { final DatabaseController controller; FilterController(this.controller); @override Future> list() async { final Database db = await controller.database; final List> maps = await db.query( 'filters', orderBy: 'name', ); return List.generate(maps.length, (i) { return Filter.fromMap(maps[i]); }); } @override Future get(dynamic identifier) async { Filter? model; final Database db = await controller.database; final List maps = await db.query( 'filters', columns: [ 'id', 'name', 'priorities', 'projects', 'contexts', 'order', 'filter', 'group', ], where: 'id = ?', whereArgs: [identifier as int], ); if (maps.isNotEmpty) { model = Filter.fromMap(maps.first); } return model; } @override Future insert(Filter model) async { final Database db = await controller.database; Map modelMap = model.toMap(); modelMap['id'] = null; // Ignore id in insert mode. final int id = await db.insert( 'filters', modelMap, conflictAlgorithm: ConflictAlgorithm.ignore, ); return id; } @override Future update(Filter model) async { final Database db = await controller.database; // @todo // if (model.id == null) { // throw SqfliteDatabaseException('Missing id attribute in Filter model'); // } final int id = await db.update( 'filters', model.toMap(), // Ensure that the model has a matching id. where: 'id = ?', // Pass the models id as a whereArg to prevent SQL injection. whereArgs: [model.id], ); return id; } @override Future delete(dynamic identifier) async { final Database db = await controller.database; final int id = await db.delete( 'filters', // Ensure that the model has a matching id. where: 'id = ?', // Pass the models id as a whereArg to prevent SQL injection. whereArgs: [identifier], ); return id; } } ================================================ FILE: lib/filter/model/filter_model.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priorities, Priority, Todo; enum ListOrder { ascending, descending, } enum ListFilter { all, completedOnly, incompletedOnly, } enum ListGroup { none, upcoming, priority, project, context, } extension Order on ListOrder { static Set get types => { for (var t in ListOrder.values) t, }; static ListOrder byName(String? name) { if (name != null) { try { return ListOrder.values.byName(name); } on ArgumentError { // Returns ListOrder.ascending } } return ListOrder.ascending; } // A negative integer if a is smaller than b, // zero if a is equal to b, and // a positive integer if a is greater than b. int _sort(T a, T b) { switch (this) { case ListOrder.ascending: return ascending(a, b); // return a.toString().compareTo(b.toString()); case ListOrder.descending: return descending(a, b); default: // Default is ascending. return ascending(a, b); } } int ascending(T a, T b) { if (a == null) { return 1; } if (b == null) { return -1; } if (a is Todo && b is Todo) { return a.description.toLowerCase().compareTo(b.description.toLowerCase()); } else { return a.toString().compareTo(b.toString()); } } int descending(T a, T b) { if (a == null) { return -1; } if (b == null) { return 1; } if (a is Todo && b is Todo) { return b.description.toLowerCase().compareTo(a.description.toLowerCase()); } else { return b.toString().compareTo(a.toString()); } } Iterable sort(Iterable list) => list.toList()..sort(_sort); } extension Filters on ListFilter { static Set get types => { for (var f in ListFilter.values) f, }; static ListFilter byName(String? name) { if (name != null) { try { return ListFilter.values.byName(name); } on ArgumentError { // Returns ListFilter.all } } return ListFilter.all; } bool _apply(Todo todo) { switch (this) { case ListFilter.all: return true; case ListFilter.completedOnly: return todo.completion; case ListFilter.incompletedOnly: return !todo.completion; default: // Default is all. return true; } } Iterable apply(Iterable todoList) => todoList.where(_apply); } extension Groups on ListGroup { static Set get types => { for (var g in ListGroup.values) g, }; static ListGroup byName(String? name) { if (name != null) { try { return ListGroup.values.byName(name); } on ArgumentError { // Returns ListGroup.none } } return ListGroup.none; } Map> groupByNone({ required Iterable todoList, }) { Map> groups = {'All': todoList}; groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections. return groups; } Map> groupByUpcoming({ required Iterable todoList, }) { Map> groups = { 'Deadline passed': todoList.where( (Todo t) { DateTime? due = t.dueDate; return (due != null && Todo.compareToToday(due) < 0) ? true : false; }, ), 'Today': todoList.where( (Todo t) { DateTime? due = t.dueDate; return (due != null && Todo.compareToToday(due) == 0) ? true : false; }, ), 'Upcoming': todoList.where( (Todo t) { DateTime? due = t.dueDate; return (due != null && Todo.compareToToday(due) > 0) ? true : false; }, ), 'No deadline': todoList.where( (Todo t) => t.dueDate == null, ), }; groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections. return groups; } Map> groupByPriority({ required Iterable todoList, required Set sections, }) { Map> groups = {}; for (var p in sections) { final Iterable items = todoList.where((Todo t) => t.priority == p); if (p == Priority.none) { groups['No priority'] = items; } else { groups[p.name] = items; } } groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections. return groups; } Map> groupByProject({ required Iterable todoList, required Set sections, }) { Map> groups = {}; // Consider also todos without projects. for (var p in [...sections, null]) { Iterable items; if (p == null) { items = todoList.where((Todo t) => t.projects.isEmpty); } else { items = todoList.where((Todo t) => t.projects.contains(p)); } if (items.isNotEmpty) { groups[p ?? 'No project'] = items; } } groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections. return groups; } Map> groupByContext({ required Iterable todoList, required Set sections, }) { Map> groups = {}; // Consider also todos without contexts. for (var c in [...sections, null]) { Iterable items; if (c == null) { items = todoList.where((Todo t) => t.contexts.isEmpty); } else { items = todoList.where((Todo t) => t.contexts.contains(c)); } if (items.isNotEmpty) { groups[c ?? 'No context'] = items; } } groups.removeWhere((k, v) => v.isEmpty); // Remove empty sections. return groups; } } class Filter extends Equatable { final int? id; final String name; final Set priorities; final Set projects; final Set contexts; final ListOrder order; final ListFilter filter; final ListGroup group; const Filter({ this.id, this.name = '', this.priorities = const {}, this.projects = const {}, this.contexts = const {}, this.order = ListOrder.ascending, this.filter = ListFilter.all, this.group = ListGroup.none, }); factory Filter.fromMap(Map map) { return Filter( id: map['id'] as int, name: map['name'] as String, priorities: { for (var p in map['priorities'].split(',')..sort()) if (p != null && p.isNotEmpty) Priorities.byName(p) }, projects: { for (var p in map['projects'].split(',')..sort()) if (p != null && p.isNotEmpty) p, }, contexts: { for (var c in map['contexts'].split(',')..sort()) if (c != null && c.isNotEmpty) c, }, order: Order.byName(map['order']), filter: Filters.byName(map['filter']), group: Groups.byName(map['group']), ); } static String get tableRepr { return '''CREATE TABLE IF NOT EXISTS filters( `id` INTEGER PRIMARY KEY, `name` TEXT NOT NULL, `priorities` TEXT, `projects` TEXT, `contexts` TEXT, `order` TEXT, `filter` TEXT, `group` TEXT )'''; } Iterable apply(List todoList) { Iterable filtered = filter.apply(todoList); if (priorities.isNotEmpty) { filtered = filtered.where(_applyPriority); } if (projects.isNotEmpty) { filtered = filtered.where(_applyProject); } if (contexts.isNotEmpty) { filtered = filtered.where(_applyContext); } // Completed todos come always at last. filtered = order.sort(filtered); return (filtered.where((Todo t) => !t.completion).toList() + filtered.where((Todo t) => t.completion).toList()); } bool _applyPriority(Todo todo) => priorities.contains(todo.priority); bool _applyProject(Todo todo) { for (String p in projects) { if (todo.projects.contains(p)) { return true; } } return false; } bool _applyContext(Todo todo) { for (String c in contexts) { if (todo.contexts.contains(c)) { return true; } } return false; } Map> grouped(Iterable todoList) { switch (group) { case ListGroup.none: return group.groupByNone(todoList: todoList); case ListGroup.upcoming: return group.groupByUpcoming(todoList: todoList); case ListGroup.priority: return group.groupByPriority( todoList: todoList, sections: order.sort(Priority.values).toSet(), ); case ListGroup.project: final Set projects = todoList.map((Todo todo) => todo.projects).fold>( {}, (Set previousValue, Set value) { return previousValue..addAll(value); }, ); return group.groupByProject( todoList: todoList, sections: order.sort(projects).toSet(), ); case ListGroup.context: Set contexts = todoList.map((Todo todo) => todo.contexts).fold>( {}, (Set previousValue, Set value) { return previousValue..addAll(value); }, ); return group.groupByContext( todoList: todoList, sections: order.sort(contexts).toSet(), ); default: // Default is none. return group.groupByNone(todoList: todoList); } } Filter copyWith({ int? id, String? name, Set? priorities, Set? projects, Set? contexts, ListOrder? order, ListFilter? filter, ListGroup? group, }) { return Filter( id: id ?? this.id, name: name ?? this.name, priorities: Priorities.sort(priorities ?? this.priorities), projects: projects != null ? (projects.toList()..sort()).toSet() : this.projects, contexts: contexts != null ? (contexts.toList()..sort()).toSet() : this.contexts, order: order ?? this.order, filter: filter ?? this.filter, group: group ?? this.group, ); } Filter copyWithUnsaved({ Set? priorities, Set? projects, Set? contexts, ListOrder? order, ListFilter? filter, ListGroup? group, }) { return Filter( priorities: priorities ?? this.priorities, projects: projects ?? this.projects, contexts: contexts ?? this.contexts, order: order ?? this.order, filter: filter ?? this.filter, group: group ?? this.group, ); } /// Convert a [Filter] into Map. /// The keys must correspond to the names of the /// columns in the database. Map toMap() { return { 'id': id, 'name': name, 'priorities': [for (var p in priorities) p.name].join(','), 'projects': projects.join(','), 'contexts': contexts.join(','), 'order': order.name, 'filter': filter.name, 'group': group.name, }; } @override String toString() { return 'Filter { id: $id, name: $name order: ${order.name} filter: ${filter.name} group: ${group.name} priorities: ${[ for (var p in priorities) p.name ]} projects: ${[for (var p in projects) p]} contexts: ${[ for (var c in contexts) c ]} }'; } @override List get props => [ id, name, priorities, projects, contexts, order, filter, group, ]; } ================================================ FILE: lib/filter/page/filter_create_edit_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart' show SnackBarHandler; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; class FilterCreateEditPage extends StatelessWidget { final Filter? initFilter; final Set projects; final Set contexts; const FilterCreateEditPage({ this.initFilter, this.projects = const {}, this.contexts = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: initFilter, ), child: GestureDetector( onTap: () { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: FilterDialogWrapper( newFilter: initFilter == null, child: Scaffold( appBar: MainAppBar( title: initFilter == null ? 'Create' : 'Edit', toolbar: Row( children: [ if (initFilter != null) const DeleteFilterIconButton(), SaveFilterIconButton(initFilter: initFilter), ], ), ), body: ListView( children: [ const FilterNameTextField(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'General', style: Theme.of(context).textTheme.titleSmall, ), ), ), const FilterOrderItem(), const FilterFilterItem(), const FilterGroupItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Tags', style: Theme.of(context).textTheme.titleSmall, ), ), ), const FilterPrioritiesItem(), FilterProjectTagsItem(availableTags: projects), FilterContextTagsItem(availableTags: contexts), const SizedBox(height: 16), ], ), ), ), ), ); } } class FilterDialogWrapper extends StatelessWidget { final Widget child; final bool newFilter; const FilterDialogWrapper({ required this.child, required this.newFilter, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, T? result) async { if (didPop) { return; } if (state.filter.name.isEmpty) { if (!await ConfirmationDialog.dialog( context: context, title: newFilter ? 'Create filter' : 'Edit filter', message: 'Cannot save a filter with an empty name.', cancelLabel: 'Cancel', actionLabel: 'Continue', )) { if (context.mounted) { context.pop(); } } } else { if (state.changed) { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Save filter', message: 'Filter contains unsaved changes. These will be irrecoverably lost.', actionLabel: 'Save', cancelLabel: 'Discard', ); if (context.mounted && confirm) { if (newFilter) { await context.read().create(state.filter); if (context.mounted) { SnackBarHandler.info(context, 'Filter has been created'); } } else { await context.read().update(state.filter); if (context.mounted) { SnackBarHandler.info(context, 'Filter has been updated'); } } } } if (context.mounted) { context.pop(); } } }, child: child, ); }, ); } } class SaveFilterIconButton extends StatelessWidget { final Filter? initFilter; const SaveFilterIconButton({ required this.initFilter, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return Visibility( visible: state.changed && state.filter.name.isNotEmpty, child: IconButton( tooltip: 'Save', icon: const Icon(Icons.save), onPressed: () async { if (initFilter == null) { await context.read().create(state.filter); } else { await context.read().update(state.filter); } if (context.mounted) { SnackBarHandler.info(context, 'Filter saved'); context.pop(); } }, ), ); }, ); } } class DeleteFilterIconButton extends StatelessWidget { const DeleteFilterIconButton({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return IconButton( tooltip: 'Delete', icon: const Icon(Icons.delete), onPressed: () async { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Delete filter', message: 'Do you want to delete the filter?', actionLabel: 'Delete', cancelLabel: 'Cancel', ); if (context.mounted && confirm) { await context.read().delete(state.filter); if (context.mounted) { SnackBarHandler.info(context, 'Filter has been deleted'); context.pop(); } } }, ); }, ); } } class FilterNameTextField extends StatefulWidget { const FilterNameTextField({super.key}); @override State createState() => _FilterNameTextFieldState(); } class _FilterNameTextFieldState extends State { late GlobalKey _textFormKey; late TextEditingController _controller; @override void initState() { super.initState(); _textFormKey = GlobalKey(); _controller = TextEditingController(); } @override void dispose() { // Clean up the controller when the widget is disposed. _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.name != state.filter.name; }, builder: (BuildContext context, FilterState state) { // Setting text and selection together. int base = _controller.selection.base.offset; _controller.value = _controller.value.copyWith( text: state.filter.name, selection: TextSelection.fromPosition( TextPosition( offset: base < 0 || base > state.filter.name.length ? state.filter.name.length : base, ), ), ); return TextFormField( key: _textFormKey, controller: _controller, minLines: 1, maxLines: 1, keyboardType: TextInputType.text, inputFormatters: [ FilteringTextInputFormatter.deny(RegExp(r'\n')), ], style: Theme.of(context).textTheme.titleMedium, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( hintText: 'Filter name', contentPadding: EdgeInsets.symmetric( horizontal: 20.0, vertical: 16.0, ), ), onChanged: (String value) => context.read().updateName(value), ); }, ); } } class FilterOrderItem extends StatelessWidget { const FilterOrderItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.order != state.filter.order; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.sort), title: const Text('Order'), subtitle: GenericChipGroup( children: [ BasicChip(label: state.filter.order.name), ], ), onTap: () async { await FilterStateOrderDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class FilterFilterItem extends StatelessWidget { const FilterFilterItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.filter != state.filter.filter; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.filter_list), title: const Text('Filter'), subtitle: GenericChipGroup( children: [ BasicChip(label: state.filter.filter.name), ], ), onTap: () async { await FilterStateFilterDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class FilterGroupItem extends StatelessWidget { const FilterGroupItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.group != state.filter.group; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.workspaces_outlined), title: const Text('Group by'), subtitle: GenericChipGroup( children: [ BasicChip( label: state.filter.group.name, ), ], ), onTap: () async { await FilterStateGroupDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class FilterPrioritiesItem extends StatelessWidget { const FilterPrioritiesItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.priorities != state.filter.priorities; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.flag_outlined), title: const Text('Priorities'), subtitle: state.filter.priorities.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.filter.priorities) BasicChip(label: t.name), ], ), onTap: () async { await FilterPriorityTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: Priority.values.toSet(), ); }, ), ); }, ); } } class FilterProjectTagsItem extends StatelessWidget { final Set availableTags; const FilterProjectTagsItem({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.projects != state.filter.projects; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.rocket_launch_outlined), title: const Text('Projects'), subtitle: state.filter.projects.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.filter.projects) BasicChip(label: t), ], ), onTap: () async { await FilterProjectTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: availableTags, ); }, ), ); }, ); } } class FilterContextTagsItem extends StatelessWidget { final Set availableTags; const FilterContextTagsItem({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) { return previousState.filter.contexts != state.filter.contexts; }, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( leading: const Icon(Icons.join_inner), title: const Text('Contexts'), subtitle: state.filter.contexts.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.filter.contexts) BasicChip(label: t), ], ), onTap: () async { await FilterContextTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: availableTags, ); }, ), ); }, ); } } ================================================ FILE: lib/filter/page/filter_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart' show PopScopeDrawer, SnackBarHandler; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; import 'package:ntodotxt/common/widget/scroll_to_top.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/filter/state/filter_list_state.dart'; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/setting/state/interaction_settings_state.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; class FilterListPage extends StatelessWidget { const FilterListPage({super.key}); @override Widget build(BuildContext context) { // @todo: Activate WideLayout later! return const FilterListViewNarrow(); // final bool isNarrowLayout = // MediaQuery.of(context).size.width < maxScreenWidthCompact; // return isNarrowLayout // ? const FilterListViewNarrow() // : const FilterListViewWide(); } } /// /// Narrow layout /// class FilterListViewNarrow extends ScollToTopView { const FilterListViewNarrow({super.key}); @override State createState() => _FilterListViewNarrowState(); } class _FilterListViewNarrowState extends ScollToTopViewState { @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterListState state) { return PopScopeDrawer( child: Scaffold( appBar: const MainAppBar(title: 'Filters'), body: ListView.builder( controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 8.0), itemCount: state.filterList.length, itemBuilder: (BuildContext context, int index) { return FilterListTile(filter: state.filterList[index]); }, ), drawer: Container(), floatingActionButton: scrolledDown ? FloatingActionButton.small( tooltip: 'Go to top', child: const Icon(Icons.keyboard_arrow_up), onPressed: () => scrollToTop(), ) : FloatingActionButton( tooltip: 'Add filter', child: const Icon(Icons.add), onPressed: () => context.pushNamed('filter-create'), ), ), ); }, ); } } /// /// Wide layout /// class FilterListViewWide extends ScollToTopView { const FilterListViewWide({super.key}); @override State createState() => _FilterListViewWideState(); } class _FilterListViewWideState extends ScollToTopViewState { @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterListState state) { return PopScopeDrawer( child: Scaffold( appBar: const MainAppBar(title: 'Filters'), body: ListView.builder( controller: scrollController, padding: const EdgeInsets.symmetric(horizontal: 8.0), itemCount: state.filterList.length, itemBuilder: (BuildContext context, int index) { return FilterListTile(filter: state.filterList[index]); }, ), floatingActionButton: scrolledDown ? FloatingActionButton.small( tooltip: 'Go to top', child: const Icon(Icons.keyboard_arrow_up), onPressed: () => scrollToTop(), ) : FloatingActionButton( tooltip: 'Add filter', child: const Icon(Icons.add), onPressed: () => context.pushNamed('filter-create'), ), ), ); }, ); } } /// /// Components /// class FilterListTile extends StatelessWidget { final Filter filter; const FilterListTile({ required this.filter, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (InteractionSettingsState previousState, InteractionSettingsState state) => previousState.swipeRightActionEnabled != state.swipeRightActionEnabled, builder: (BuildContext context, InteractionSettingsState state) { return Dismissible( key: ValueKey(filter.id!), direction: state.swipeRightActionEnabled ? DismissDirection.startToEnd : DismissDirection.none, dismissThresholds: const { DismissDirection.startToEnd: 0.5, DismissDirection.endToStart: 0.5, }, background: Container( color: Theme.of(context).colorScheme.error, // red alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 16.0), child: Icon(Icons.delete), ), confirmDismiss: (DismissDirection direction) async { // Delete if (direction == DismissDirection.startToEnd) { return await ConfirmationDialog.dialog( context: context, title: 'Delete filter', message: 'Do you want to delete the filter?', actionLabel: 'Delete', cancelLabel: 'Cancel', ); } return true; }, onDismissed: (DismissDirection direction) async { if (direction == DismissDirection.startToEnd) { // Delete context.read().add( FilterListFilterDeleted(filter: filter), ); SnackBarHandler.info(context, 'Filter has been deleted'); } }, child: ListTile( key: ValueKey(filter.id!), title: Text( filter.name, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: _buildSubtitle(), onTap: () => context.pushNamed('filter-edit', extra: filter), ), ); }, ); } Widget? _buildSubtitle() { final List items = [ filter.order.name, filter.filter.name, filter.group.name, for (Priority p in filter.priorities) p.name, for (String p in filter.projects) '+$p', for (String c in filter.contexts) '@$c', ]..removeWhere((value) => value.isEmpty); if (items.isEmpty) { return null; } List shortenedItems; if (items.length > 5) { shortenedItems = items.sublist(0, 5); shortenedItems.add('...'); } else { shortenedItems = [...items]; } return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 4.0, // gap between adjacent chips runSpacing: 4.0, // gap between lines children: [ for (String attr in shortenedItems) BasicChip(label: attr, mono: true), ], ); } } ================================================ FILE: lib/filter/repository/filter_repository.dart ================================================ import 'package:ntodotxt/filter/controller/filter_controller.dart' show FilterController; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:rxdart/rxdart.dart'; class FilterRepository { final FilterController controller; final BehaviorSubject> _streamController = BehaviorSubject>.seeded(const []); FilterRepository(this.controller); Stream> get stream => _streamController.asBroadcastStream(); Future refresh() async { _streamController.sink.add(await controller.list()); } void dispose() { _streamController.close(); } Future> list() async => await controller.list(); Future get({required int id}) async => await controller.get(id); Future insert(Filter model) async { final int result = await controller.insert(model); await refresh(); return result; } Future update(Filter model) async { final int result = await controller.update(model); await refresh(); return result; } Future delete({required int id}) async { final int result = await controller.delete(id); await refresh(); return result; } } ================================================ FILE: lib/filter/state/filter_cubit.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter, Filters, Groups, ListFilter, ListGroup, ListOrder, Order; import 'package:ntodotxt/filter/repository/filter_repository.dart' show FilterRepository; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/model/setting_model.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; class FilterCubit extends Cubit { final SettingRepository _settingRepository; final FilterRepository _filterRepository; FilterCubit({ required SettingRepository settingRepository, required FilterRepository filterRepository, Filter? filter, }) : _settingRepository = settingRepository, _filterRepository = filterRepository, super( filter == null ? FilterLoading(filter: const Filter()) : FilterSaved(filter: filter), ); Future load() async { try { if (state is FilterLoading) { emit( state.save( filter: Filter( order: Order.byName( (await _settingRepository.get(key: 'order'))?.value), filter: Filters.byName( (await _settingRepository.get(key: 'filter'))?.value), group: Groups.byName( (await _settingRepository.get(key: 'group'))?.value), ), ), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } /// /// Regular filter (saved filter) /// Future create(Filter f) async { try { final Filter filter = f.copyWith(name: f.name.trim()); int id = await _filterRepository.insert(filter); if (id > 0) { emit(state.save(filter: filter.copyWith(id: id))); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future update(Filter f) async { try { final Filter filter = f.copyWith(name: f.name.trim()); int id = await _filterRepository.update(filter); if (id > 0) { emit(state.save(filter: filter)); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future delete(Filter filter) async { try { if (filter.id != null) { await _filterRepository.delete(id: filter.id!); } emit(state.save(filter: filter.copyWithUnsaved())); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateName(String name) { try { emit(state.update( filter: state.filter.copyWith( name: name.replaceAllMapped(RegExp(r'\s{2,}'), (match) => ' '), ), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateOrder(ListOrder order) { try { emit(state.update( filter: state.filter.copyWith(order: order), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateFilter(ListFilter filter) { try { emit(state.update( filter: state.filter.copyWith(filter: filter), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateGroup(ListGroup group) { try { emit(state.update( filter: state.filter.copyWith(group: group), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addPriority(Priority priority) { try { emit( state.update( filter: state.filter.copyWith( priorities: {...state.filter.priorities, priority}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removePriority(Priority priority) { try { emit( state.update( filter: state.filter.copyWith( priorities: {...state.filter.priorities}..remove(priority), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updatePriorities(Set priorities) { try { emit( state.update( filter: state.filter.copyWith( priorities: {...priorities}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addProject(String project) { try { emit( state.update( filter: state.filter.copyWith( projects: {...state.filter.projects, project}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removeProject(String project) { try { emit( state.update( filter: state.filter.copyWith( projects: {...state.filter.projects}..remove(project), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateProjects(Set projects) { try { emit( state.update( filter: state.filter.copyWith( projects: {...projects}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addContext(String context) { try { emit( state.update( filter: state.filter.copyWith( contexts: {...state.filter.contexts, context}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removeContext(String context) { try { emit( state.update( filter: state.filter.copyWith( contexts: {...state.filter.contexts}..remove(context), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateContexts(Set contexts) { try { emit( state.update( filter: state.filter.copyWith( contexts: {...contexts}, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } /// /// Default filter /// Future resetToDefaults() async { try { const Filter defaultFilter = Filter(); for (var k in ['order', 'filter', 'group']) { await _settingRepository.delete(key: k); } emit(state.save(filter: defaultFilter)); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future updateDefaultOrder(ListOrder? value) async { try { if (value != null) { emit( state.save( filter: state.filter.copyWith(order: value), ), ); await _settingRepository.updateOrInsert( Setting(key: 'order', value: value.name), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future updateDefaultFilter(ListFilter? value) async { try { if (value != null) { emit( state.save( filter: state.filter.copyWith(filter: value), ), ); await _settingRepository.updateOrInsert( Setting(key: 'filter', value: value.name), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future updateDefaultGroup(ListGroup? value) async { try { if (value != null) { emit( state.save( filter: state.filter.copyWith(group: value), ), ); await _settingRepository.updateOrInsert( Setting(key: 'group', value: value.name), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/filter/state/filter_list_bloc.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/filter/state/filter_list_state.dart'; class FilterListBloc extends Bloc { final FilterRepository _repository; FilterListBloc({required FilterRepository repository}) : _repository = repository, super(const FilterListLoading()) { on(_onFilterListSubscriped); on(_onFilterSynchronizationRequested); on(_onFilterDeleted); } Future _onFilterListSubscriped( FilterListSubscriped event, Emitter emit, ) async { await emit.forEach>( _repository.stream, onData: (filterList) { return state.copyWith(filterList: filterList); }, onError: (e, _) => state.error(message: e.toString()), ); } Future _onFilterSynchronizationRequested( FilterListSynchronizationRequested event, Emitter emit, ) async { try { emit(state.loading()); await _repository.refresh(); emit(state.success()); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future _onFilterDeleted( FilterListFilterDeleted event, Emitter emit, ) async { final List previousList = state.filterList; final List updatedList = previousList .where((Filter item) => item.id != event.filter.id) .toList(growable: false); // Important: Remove item instantily for Dismissible. // Otherwise we get an error: // A dismissed Dismissible widget is still part of the tree. emit(state.success(filterList: updatedList)); try { await _repository.delete(id: event.filter.id!); } on Exception catch (e) { // If error rollback to previous list. emit(state.error(message: e.toString(), filterList: previousList)); } } } ================================================ FILE: lib/filter/state/filter_list_event.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; sealed class FilterListEvent extends Equatable { const FilterListEvent(); @override List get props => []; } final class FilterListSubscriped extends FilterListEvent { const FilterListSubscriped(); } final class FilterListSynchronizationRequested extends FilterListEvent { const FilterListSynchronizationRequested(); } final class FilterListFilterDeleted extends FilterListEvent { final Filter filter; const FilterListFilterDeleted({ required this.filter, }); @override List get props => [filter]; } ================================================ FILE: lib/filter/state/filter_list_state.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; sealed class FilterListState extends Equatable { final List filterList; const FilterListState({ this.filterList = const [], }); FilterListState copyWith({ List? filterList, }); FilterListLoading loading({ List? filterList, }) { return FilterListLoading( filterList: filterList ?? this.filterList, ); } FilterListSuccess success({ List? filterList, }) { return FilterListSuccess( filterList: filterList ?? this.filterList, ); } FilterListError error({ required String message, List? filterList, }) { return FilterListError( message: message, filterList: filterList ?? this.filterList, ); } @override List get props => [ filterList, ]; @override String toString() => 'FilterListState { filters: $filterList }'; } final class FilterListLoading extends FilterListState { const FilterListLoading({ super.filterList, }); @override FilterListLoading copyWith({List? filterList}) => super.loading(filterList: filterList ?? this.filterList); @override String toString() => 'FilterListLoading { filters: $filterList }'; } final class FilterListSuccess extends FilterListState { const FilterListSuccess({ super.filterList, }); @override FilterListSuccess copyWith({List? filterList}) => super.success(filterList: filterList ?? this.filterList); @override String toString() => 'FilterListSuccess { filters: $filterList }'; } final class FilterListError extends FilterListState { final String message; const FilterListError({ required this.message, super.filterList, }); @override FilterListError copyWith({ String? message, List? filterList, }) => super.error( message: message ?? this.message, filterList: filterList ?? this.filterList, ); @override List get props => [ message, filterList, ]; @override String toString() => 'FilterListError { message: $message filters: $filterList }'; } ================================================ FILE: lib/filter/state/filter_state.dart ================================================ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; sealed class FilterState extends Equatable { final Filter filter; final Filter? origin; FilterState({ required this.filter, Filter? origin, }) : origin = origin ?? filter.copyWith(); FilterLoading loading({ Filter? filter, }) { return FilterLoading( filter: filter ?? this.filter, origin: origin, ); } FilterChanged update({ Filter? filter, }) { return FilterChanged( filter: filter ?? this.filter, origin: origin, ); } FilterSaved save({ Filter? filter, }) { return FilterSaved( filter: filter ?? this.filter, ); } FilterError error({ required String message, Filter? filter, }) { return FilterError( message: message, filter: filter ?? this.filter, origin: origin, ); } bool get changed => origin != filter; bool get orderChanged { if (origin == null) { return true; } else { return origin!.order != filter.order; } } bool get filterChanged { if (origin == null) { return true; } else { return origin!.filter != filter.filter; } } bool get groupChanged { if (origin == null) { return true; } else { return origin!.group != filter.group; } } bool get prioritiesChanged { if (origin == null) { return true; } else { return !const SetEquality().equals(origin!.priorities, filter.priorities); } } bool get projectsChanged { if (origin == null) { return true; } else { return !const SetEquality().equals(origin!.projects, filter.projects); } } bool get contextsChanged { if (origin == null) { return true; } else { return !const SetEquality().equals(origin!.contexts, filter.contexts); } } @override List get props => [ filter, origin, ]; @override String toString() => 'FilterState { filter: $filter }'; } final class FilterLoading extends FilterState { FilterLoading({ required super.filter, super.origin, }); FilterLoading copyWith({ Filter? filter, }) => super.loading(filter: filter ?? this.filter); @override String toString() => 'FilterLoading { filter: $filter }'; } final class FilterChanged extends FilterState { FilterChanged({ required super.filter, super.origin, }); FilterChanged copyWith({ Filter? filter, }) => super.update(filter: filter ?? this.filter); @override String toString() => 'FilterChanged { filter: $filter }'; } final class FilterSaved extends FilterState { FilterSaved({ required super.filter, super.origin, }); FilterSaved copyWith({ Filter? filter, }) => super.save(filter: filter ?? this.filter); @override String toString() => 'FilterSaved { filter: $filter }'; } final class FilterError extends FilterState { final String message; FilterError({ required this.message, required super.filter, super.origin, }); FilterError copyWith({ String? message, Filter? filter, }) => super.error( message: message ?? this.message, filter: filter ?? this.filter); @override List get props => [ message, filter, origin, ]; @override String toString() => 'FilterError { message: $message filter: $filter }'; } ================================================ FILE: lib/filter/widget/filter_chip.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show ListFilter, ListOrder; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; class FilterOrderChip extends StatelessWidget { const FilterOrderChip({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.orderChanged; return GenericActionChip( avatar: const Icon(Icons.sort_by_alpha), label: Row( children: [ Text(state.filter.order == ListOrder.ascending ? 'asc' : 'desc'), const Text(' '), Icon( state.filter.order == ListOrder.ascending ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18.0, ), ], ), selected: changed, onPressed: () async { await FilterStateOrderDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ); } } class FilterFilterChip extends StatelessWidget { const FilterFilterChip({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.filterChanged; late final String label; late final IconData iconData; switch (state.filter.filter) { case ListFilter.all: label = 'all'; iconData = Icons.filter_list; break; case ListFilter.completedOnly: label = 'completed'; iconData = Icons.done_all; break; case ListFilter.incompletedOnly: label = 'incompleted'; iconData = Icons.remove_done; break; } return GenericActionChip( avatar: Icon(iconData), label: Text(label), selected: changed, onPressed: () async { await FilterStateFilterDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ); } } class FilterGroupChip extends StatelessWidget { const FilterGroupChip({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.groupChanged; return GenericActionChip( avatar: Icon(changed ? Icons.workspaces : Icons.workspaces_outlined), label: Text(state.filter.group.name), selected: changed, onPressed: () async { await FilterStateGroupDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ); } } class FilterPrioritiesChip extends StatelessWidget { const FilterPrioritiesChip({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.prioritiesChanged; return GenericActionChip( avatar: Icon(changed ? Icons.flag : Icons.flag_outlined), label: const Text('priorities'), selected: changed, onPressed: () async { await FilterPriorityTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: Priority.values.toSet(), ); }, ); }, ); } } class FilterProjectsChip extends StatelessWidget { final Set availableTags; const FilterProjectsChip({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.projectsChanged; return GenericActionChip( avatar: Icon( changed ? Icons.rocket_launch : Icons.rocket_launch_outlined), label: const Text('projects'), selected: changed, onPressed: () async { await FilterProjectTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: {...availableTags, ...state.filter.projects}, ); }, ); }, ); } } class FilterContextsChip extends StatelessWidget { final Set availableTags; const FilterContextsChip({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { final bool changed = state.contextsChanged; return GenericActionChip( avatar: const Icon(Icons.tag), label: const Text('contexts'), selected: changed, onPressed: () async { await FilterContextTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: {...availableTags, ...state.filter.contexts}, ); }, ); }, ); } } ================================================ FILE: lib/intro/page/intro_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/login/page/login_page.dart'; import 'package:url_launcher/url_launcher.dart'; class IntroPage extends StatefulWidget { const IntroPage({super.key}); @override State createState() => _IntroPageState(); } class _IntroPageState extends State { int _currentPage = 0; final PageController _pageController = PageController(initialPage: 0); final List pages = [ const IntroPageWelcome(), IntroPageLocal(), IntroPageWebDav(), ]; @override Widget build(BuildContext context) { return Scaffold( body: PageView( controller: _pageController, onPageChanged: (int page) { setState(() { _currentPage = page; }); }, children: pages, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: Stack( fit: StackFit.expand, children: [ Positioned( left: 30, bottom: 20, child: IconButton( tooltip: 'Previous page', icon: const Icon( Icons.navigate_before, size: 40, ), onPressed: _currentPage > 0 ? () { setState(() { _currentPage -= 1; }); _pageController.previousPage( duration: const Duration(milliseconds: 500), curve: Curves.ease, ); } : null, ), ), Positioned( bottom: 20, right: 30, child: IconButton( tooltip: 'Next page', icon: const Icon( Icons.navigate_next, size: 40, ), onPressed: _currentPage < pages.length - 1 ? () { setState(() { _currentPage += 1; }); _pageController.nextPage( duration: const Duration(milliseconds: 500), curve: Curves.ease, ); } : null, ), ), ], ), ); } } class IntroPageWelcome extends StatelessWidget { const IntroPageWelcome({super.key}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Welcome', style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 24), Text( '''This app is based on the todo.txt format. If you don't know what this is, you can find out more about the format below.''', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 24), FilledButton.tonal( child: const Text('todo.txt format'), onPressed: () async { const String urlStr = 'https://github.com/todotxt/todo.txt'; final Uri url = Uri.parse(urlStr); if (!await launchUrl(url)) { throw Exception('Could not open $urlStr'); } }, ), const SizedBox(height: 24), Text( 'You can use this app in different ways. Find out more on the following pages.', style: Theme.of(context).textTheme.bodyLarge, ), ], ), ); } } class IntroPageLocal extends StatelessWidget { IntroPageLocal({super.key}); final List<(String, TextStyle?)> content = [ ('Use this app in ', null), ('local', const TextStyle(fontWeight: FontWeight.bold)), (' or ', null), ('cloudless', const TextStyle(fontWeight: FontWeight.bold)), (' mode.', null), ( '\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.', null ), ( '\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.).', null ), ]; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Local', style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 24), RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyLarge, text: '', children: [ for ((String, TextStyle?) item in content) TextSpan(text: item.$1, style: item.$2), ], ), ), const SizedBox(height: 24), FilledButton.tonal( child: const Text('Use local mode'), onPressed: () async { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const LocalLoginView(), ), ); }, ), ], ), ); } } class IntroPageWebDav extends StatelessWidget { IntroPageWebDav({super.key}); final List<(String, TextStyle?)> content = [ ('Use this app in ', null), ('webdav', const TextStyle(fontWeight: FontWeight.bold)), (' or ', null), ('cloud', const TextStyle(fontWeight: FontWeight.bold)), (' mode.', null), ( '\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.', null ) ]; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'WebDAV', style: Theme.of(context).textTheme.headlineLarge, ), const SizedBox(height: 24), RichText( text: TextSpan( style: Theme.of(context).textTheme.bodyLarge, text: '', children: [ for ((String, TextStyle?) item in content) TextSpan(text: item.$1, style: item.$2), ], ), ), const SizedBox(height: 24), FilledButton.tonal( child: const Text('Use webdav mode'), onPressed: () async { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) => const WebDAVLoginView(), ), ); }, ), ], ), ); } } ================================================ FILE: lib/licenses/page/licenses_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/oss_licenses.dart'; class LicenceListPage extends StatelessWidget { const LicenceListPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold( appBar: MainAppBar(title: 'Licenses'), body: LicenseListView(), ); } } class LicenseListView extends StatelessWidget { const LicenseListView({super.key}); @override Widget build(BuildContext context) { return ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 8.0), itemCount: allDependencies.length, itemBuilder: (BuildContext context, int index) { Package package = allDependencies[index]; return ListTile( title: Text(package.name), subtitle: Text(package.repository ?? (package.homepage ?? '')), onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { return LicenceDetailPage( title: package.name, licence: package.license ?? package.description, ); }, ), ); }, ); }, ); } } class LicenceDetailPage extends StatelessWidget { final String title; final String licence; const LicenceDetailPage({ super.key, required this.title, required this.licence, }); @override Widget build(BuildContext context) { return Scaffold( appBar: MainAppBar(title: title), body: Center( child: Container( padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ Text(licence), ], ), ), ), ), ); } } ================================================ FILE: lib/login/page/login_page.dart ================================================ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/constants/app.dart'; import 'package:ntodotxt/common/misc.dart'; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/info_dialog.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; import 'package:permission_handler/permission_handler.dart'; class LocalLoginView extends StatefulWidget { const LocalLoginView({super.key}); @override State createState() => _LocalLoginViewState(); } class _LocalLoginViewState extends State { bool loading = false; @override Widget build(BuildContext context) { bool keyboardIsOpen = MediaQuery.of(context).viewInsets.bottom != 0; return Stack( children: [ Scaffold( appBar: const MainAppBar(title: 'Local'), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [ ListTile( title: Text( 'Todo', style: Theme.of(context).textTheme.titleSmall, ), ), const TodoFilenameInput(), const LocalPathInput(), ], ), floatingActionButton: keyboardIsOpen ? null : BlocBuilder( builder: (BuildContext context, TodoFileState state) { return FloatingActionButton.extended( heroTag: 'localUsage', icon: const Icon(Icons.done), label: const Text('Apply'), tooltip: 'Apply', onPressed: () async { try { setState(() => loading = true); await context.read().loginLocal( localTodoFilePath: state.localTodoFilePath, ); } finally { setState(() => loading = false); } }, ); }, ), ), if (loading) const Opacity( opacity: 0.8, child: ModalBarrier(dismissible: false, color: Colors.black), ), if (loading) const Center( child: CircularProgressIndicator(), ), ], ); } } class WebDAVLoginView extends StatefulWidget { const WebDAVLoginView({super.key}); @override State createState() => _WebDAVLoginViewState(); } class _WebDAVLoginViewState extends State { bool loading = false; String serverAddr = ''; String path = ''; String username = ''; String password = ''; bool acceptUntrustedCert = false; late GlobalKey formKey; @override void initState() { super.initState(); formKey = GlobalKey(); } @override void dispose() { super.dispose(); } void setServerAddr(String value) => setState(() => serverAddr = value); void setAcceptUntrustedCert(bool value) => setState(() => acceptUntrustedCert = value); void setBaseUrl(String value) => setState(() => path = value); void setUsername(String value) => setState(() => username = value); void setPassword(String value) => setState(() => password = value); @override Widget build(BuildContext context) { bool keyboardIsOpen = MediaQuery.of(context).viewInsets.bottom != 0; return GestureDetector( onTap: () { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: Stack( children: [ Scaffold( appBar: const MainAppBar(title: 'WebDAV'), body: Form( key: formKey, child: ListView( padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [ ListTile( title: Text( 'Server connection', style: Theme.of(context).textTheme.titleSmall, ), ), ServerAddrField( callback: setServerAddr, ), AcceptUntrustedCertField( callback: setAcceptUntrustedCert, ), BaseUrlField( callback: setBaseUrl, ), UsernameField( callback: setUsername, ), PasswordField( callback: setPassword, ), const Divider(), ListTile( title: Text( 'Todo', style: Theme.of(context).textTheme.titleSmall, ), ), const TodoFilenameInput(), const LocalPathInput(), const RemotePathInput(), const SizedBox(height: 72), ], ), ), floatingActionButton: keyboardIsOpen ? null : BlocBuilder( builder: (BuildContext context, TodoFileState state) { return FloatingActionButton.extended( heroTag: 'webdavUsage', icon: const Icon(Icons.done), label: const Text('Apply'), tooltip: 'Apply', onPressed: () async { if (formKey.currentState!.validate()) { try { setState(() => loading = true); await context.read().loginWebDAV( localTodoFilePath: state.localTodoFilePath, remoteTodoFilePath: state.remoteTodoFilePath, server: serverAddr, path: path, username: username, password: password, acceptUntrustedCert: acceptUntrustedCert, ); } finally { setState(() => loading = false); } } }, ); }, ), ), if (loading) const Opacity( opacity: 0.8, child: ModalBarrier(dismissible: false, color: Colors.black), ), if (loading) const Center( child: CircularProgressIndicator(), ), ], ), ); } } class ServerAddrField extends StatefulWidget { final Function(String serverAddr) callback; const ServerAddrField({ required this.callback, super.key, }); @override State createState() => _ServerAddrFieldState(); } class _ServerAddrFieldState extends State { late TextEditingController textFieldController; @override void initState() { super.initState(); textFieldController = TextEditingController(); } @override void dispose() { textFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListTile( leading: const Icon(Icons.dns), title: TextFormField( controller: textFieldController, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Server', hintText: 'http[s]://server[:port]', ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Missing server address'; } if (!value.startsWith('http://') && !value.startsWith('https://')) { return 'Missing protocol'; } if (!RegExp( r'(?^(http|https):\/\/)(?[a-zA-Z0-9.-]+)(:(?\d+)){0,1}$') .hasMatch(value)) { return 'Invalid format'; } return null; }, onChanged: (String value) => widget.callback(textFieldController.text), ), ); } } class AcceptUntrustedCertField extends StatefulWidget { final Function(bool allowSelfSignedCert) callback; const AcceptUntrustedCertField({ required this.callback, super.key, }); @override State createState() => _AcceptUntrustedCertFieldState(); } class _AcceptUntrustedCertFieldState extends State { bool checked = false; @override Widget build(BuildContext context) { return ListTile( leading: Icon(!checked ? Icons.lock_outline : Icons.lock_open), title: const Text('Allow certificate'), subtitle: const Text('Auto-accept certificate'), trailing: Checkbox( value: checked, onChanged: (bool? value) { setState(() => checked = value ?? true); widget.callback(value ?? true); }, ), ); } } class BaseUrlField extends StatefulWidget { final Function(String path) callback; const BaseUrlField({ required this.callback, super.key, }); @override State createState() => _BaseUrlFieldState(); } class _BaseUrlFieldState extends State { late TextEditingController textFieldController; @override void initState() { super.initState(); textFieldController = TextEditingController(); } @override void dispose() { textFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListTile( leading: const Icon(Icons.http), title: TextFormField( controller: textFieldController, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Path', hintText: '/remote.php/dav/files/', ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Missing path'; } return null; }, onChanged: (String value) => widget.callback(textFieldController.text), ), trailing: IconButton( icon: const Icon(Icons.help_outline), onPressed: () => InfoDialog.dialog( context: context, title: 'Path', message: '''The username is not automatically appended to the path. In some cases path containing the username is expected, in others this causes an error. Please check the requirements of your webdav server.''', ), ), ); } } class UsernameField extends StatefulWidget { final Function(String username) callback; const UsernameField({ required this.callback, super.key, }); @override State createState() => _UsernameFieldState(); } class _UsernameFieldState extends State { late TextEditingController textFieldController; @override void initState() { super.initState(); textFieldController = TextEditingController(); } @override void dispose() { textFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListTile( leading: const Icon(Icons.person), title: TextFormField( controller: textFieldController, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Username', hintText: 'Username', ), validator: (String? value) { if (value == null || value.isEmpty) { return 'Missing username'; } return null; }, onChanged: (String value) => widget.callback(textFieldController.text), ), ); } } class PasswordField extends StatefulWidget { final Function(String password) callback; const PasswordField({ required this.callback, super.key, }); @override State createState() => _PasswordFieldState(); } class _PasswordFieldState extends State { late TextEditingController textFieldController; bool showPassword = false; @override void initState() { super.initState(); textFieldController = TextEditingController(); } @override void dispose() { textFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListTile( leading: const Icon(Icons.password), title: TextFormField( controller: textFieldController, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Password', hintText: 'Password', ), obscureText: !showPassword, validator: (String? value) { if (value == null || value.isEmpty) { return 'Missing password'; } return null; }, onChanged: (String value) => widget.callback(textFieldController.text), ), trailing: IconButton( icon: Icon( showPassword ? Icons.visibility_off : Icons.visibility, ), onPressed: () => setState(() => showPassword = !showPassword), ), ); } } class LocalPathInput extends StatelessWidget { const LocalPathInput({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoFileState previousState, TodoFileState state) => previousState.localPath != state.localPath, builder: (BuildContext context, TodoFileState state) { return ListTile( leading: const Icon(Icons.folder), title: Text( 'Local path', style: Theme.of(context).textTheme.bodySmall, ), subtitle: Text(state.localPath), trailing: IconButton( icon: const Icon(Icons.help_outline), onPressed: () => InfoDialog.dialog( context: context, title: 'Local path', message: '''Choose a directory by tapping the current local path. Use this option if it's important to you where your todos are stored on your device. Otherwise, the app's cache directory is used.''', ), ), onTap: () async { if (!PlatformInfo.isAppOS || await Permission.manageExternalStorage.request().isGranted || await Permission.storage.request().isGranted) { String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); if (context.mounted) { // If user canceled the directory picker use app cache directory as fallback. await context .read() .saveLocalPath(selectedDirectory ?? state.localPath); } } }, ); }, ); } } class RemotePathInput extends StatefulWidget { const RemotePathInput({super.key}); @override State createState() => _RemotePathInputState(); } class _RemotePathInputState extends State { final TextEditingController controller = TextEditingController(); final Debouncer debounce = Debouncer(milliseconds: 1000); @override void initState() { super.initState(); } @override void dispose() { controller.dispose(); debounce.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Initial value controller.text = context.read().state.remotePath; return BlocBuilder( builder: (BuildContext context, TodoFileState state) { return ListTile( leading: const Icon(Icons.folder), title: TextFormField( controller: controller, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Remote path', hintText: defaultRemoteTodoPath, ), onChanged: (String value) async { debounce.run(() async => await _save(context, value)); }, ), trailing: IconButton( icon: const Icon(Icons.help_outline), onPressed: () => InfoDialog.dialog( context: context, title: 'Remote path', message: '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.', ), )); }, ); } Future _save(BuildContext context, String value) async { if (value.isEmpty) { SnackBarHandler.info( context, 'Empty remote path is not allowed. Using default one.', ); await context.read().saveRemotePath(defaultRemoteTodoPath); controller.value = controller.value.copyWith( text: defaultRemoteTodoPath, selection: const TextSelection.collapsed(offset: defaultRemoteTodoPath.length), ); } else { await context.read().saveRemotePath(value); controller.value = controller.value.copyWith( text: value, selection: TextSelection.collapsed(offset: value.length), ); } } } class TodoFilenameInput extends StatefulWidget { const TodoFilenameInput({super.key}); @override State createState() => _TodoFilenameInputState(); } class _TodoFilenameInputState extends State { final TextEditingController controller = TextEditingController(); final Debouncer debounce = Debouncer(milliseconds: 1000); @override void initState() { super.initState(); } @override void dispose() { controller.dispose(); debounce.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Initial value controller.text = context.read().state.todoFilename; return BlocBuilder( builder: (BuildContext context, TodoFileState state) { return ListTile( leading: const Icon(Icons.description), title: TextFormField( controller: controller, style: Theme.of(context).textTheme.bodyMedium, textCapitalization: TextCapitalization.none, decoration: const InputDecoration( labelText: 'Todo filename', hintText: defaultTodoFilename, ), onChanged: (String value) async { debounce.run(() async => await _save(context, value)); }, ), ); }, ); } Future _save(BuildContext context, String value) async { if (value.isEmpty) { SnackBarHandler.info( context, 'Empty todo filename is not allowed. Using default one.', ); await context .read() .saveLocalFilename(defaultTodoFilename); controller.value = controller.value.copyWith( text: defaultTodoFilename, selection: const TextSelection.collapsed(offset: defaultTodoFilename.length), ); } else { await context.read().saveLocalFilename(value); controller.value = controller.value.copyWith( text: value, selection: TextSelection.collapsed(offset: value.length), ); } } } ================================================ FILE: lib/login/state/login_cubit.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/login/state/login_state.dart'; import 'package:ntodotxt/main.dart' show secureStorage; import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; class LoginCubit extends Cubit { LoginCubit({ LoginState? state, }) : super(state ?? const LoginLoading()); Future login() async { try { emit(state.loading()); String? backendFromsecureStorage = await secureStorage.read(key: 'backend'); Backend backend; if (backendFromsecureStorage == null) { return emit(state.logout()); } try { backend = Backend.values.byName(backendFromsecureStorage); } on Exception { return emit(state.logout()); } if (backend == Backend.none) { return emit(state.logout()); } if (backend == Backend.local) { return emit(state.loginLocal()); } if (backend == Backend.webdav) { String? server = await secureStorage.read(key: 'server'); String? path = await secureStorage.read(key: 'path'); String? username = await secureStorage.read(key: 'username'); String? password = await secureStorage.read(key: 'password'); bool acceptUntrustedCert = (await secureStorage.read(key: 'acceptUntrustedCert')) == '1' ? true : false; if (server != null && path != null && username != null && password != null) { emit( state.loginWebDAV( server: server, path: path, username: username, password: password, acceptUntrustedCert: acceptUntrustedCert, ), ); } } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future logout() async { try { await resetSecureStorage(); emit(state.logout()); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future loginLocal({ required String localTodoFilePath, }) async { try { LocalTodoListApi.fromString( localFilePath: localTodoFilePath); // Check before login. await resetSecureStorage(); await secureStorage.write(key: 'backend', value: Backend.local.name); emit(state.loginLocal()); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future loginWebDAV({ required String localTodoFilePath, required String remoteTodoFilePath, required String server, required String path, required String username, required String password, required bool acceptUntrustedCert, }) async { try { WebDAVClient client = WebDAVClient( server: server, path: path, username: username, password: password, acceptUntrustedCert: acceptUntrustedCert, ); WebDAVTodoListApi api = WebDAVTodoListApi.fromString( localFilePath: localTodoFilePath, remoteFilePath: remoteTodoFilePath, client: client, ); await api.client.ping(); await api.client.listFiles(); await resetSecureStorage(); await secureStorage.write(key: 'backend', value: Backend.webdav.name); await secureStorage.write(key: 'server', value: server); await secureStorage.write(key: 'path', value: path); await secureStorage.write(key: 'username', value: username); await secureStorage.write(key: 'password', value: password); await secureStorage.write( key: 'acceptUntrustedCert', value: acceptUntrustedCert ? '1' : '0'); emit( state.loginWebDAV( server: server, path: path, username: username, password: password, acceptUntrustedCert: acceptUntrustedCert, ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future resetSecureStorage() async { try { final List attrs = [ 'backend', 'server', 'path', 'username', 'password', 'acceptUntrustedCert', ]; for (var attr in attrs) { final String? value = await secureStorage.read(key: attr); if (value != null) { await secureStorage.delete(key: attr); } } emit(state.logout()); } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/login/state/login_state.dart ================================================ import 'dart:math'; import 'package:equatable/equatable.dart'; enum Backend { none, local, webdav } sealed class LoginState extends Equatable { final Backend backend; // Backend to use to store todos. const LoginState({ this.backend = Backend.none, }); LoginState copyWith(); LoginLoading loading() => const LoginLoading(); Logout logout() => const Logout(); LoginLocal loginLocal() => const LoginLocal(); LoginWebDAV loginWebDAV({ required String server, required String path, required String username, required String password, required bool acceptUntrustedCert, }) => LoginWebDAV( server: server, path: path, username: username, password: password, acceptUntrustedCert: acceptUntrustedCert, ); LoginError error({ required String message, }) => LoginError(message: message); @override List get props => [ backend, ]; @override String toString() => 'LoginState { }'; } final class LoginLoading extends LoginState { const LoginLoading({ super.backend, }); @override LoginLoading copyWith() => super.loading(); @override List get props => [ backend, ]; @override String toString() => 'LoginLoading { }'; } final class Logout extends LoginState { const Logout({ super.backend = Backend.none, }); @override Logout copyWith() => super.logout(); @override List get props => [ backend, ]; @override String toString() => 'Logout { }'; } final class LoginLocal extends LoginState { const LoginLocal({ super.backend = Backend.local, }); @override LoginLocal copyWith() => super.loginLocal(); @override List get props => [ backend, ]; @override String toString() => 'LoginLocal { }'; } final class LoginWebDAV extends LoginState { /// Backend server. final String server; /// Path. final String path; /// Backend username. final String username; /// Backend password. final String password; /// Accept untrusted certificate. final bool acceptUntrustedCert; const LoginWebDAV({ super.backend = Backend.webdav, required this.server, required this.path, required this.username, required this.password, required this.acceptUntrustedCert, }); @override LoginWebDAV copyWith({ String? server, String? path, String? username, String? password, bool? acceptUntrustedCert, }) => super.loginWebDAV( server: server ?? this.server, path: path ?? this.path, username: username ?? this.username, password: password ?? this.password, acceptUntrustedCert: acceptUntrustedCert ?? this.acceptUntrustedCert, ); @override List get props => [ backend, server, path, username, password, acceptUntrustedCert, ]; @override String toString() => 'LoginWebDAV { }'; } final class LoginError extends LoginState { /// Random id to force updates/rebuilds /// if there occure the same errors in row. final int id; /// Error message. final String message; LoginError({ required this.message, super.backend, }) : id = Random().nextInt(999); @override LoginError copyWith({ String? message, }) => super.error(message: message ?? this.message); @override List get props => [ id, backend, message, ]; @override String toString() => 'LoginError { message: $message }'; } ================================================ FILE: lib/main.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logging/logging.dart'; import 'package:ntodotxt/common/bloc_observer.dart' show GenericBlocObserver; import 'package:ntodotxt/common/misc.dart' show SnackBarHandler; import 'package:ntodotxt/common/router/router.dart'; import 'package:ntodotxt/common/theme/theme.dart' show lightTheme, darkTheme; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart' show FilterController; import 'package:ntodotxt/filter/repository/filter_repository.dart' show FilterRepository; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/filter/state/filter_list_state.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/intro/page/intro_page.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/login/state/login_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/todo/repository/todo_list_repository.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; final Logger log = Logger('ntodotxt'); const FlutterSecureStorage secureStorage = FlutterSecureStorage( // Pass the option to the constructor iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), aOptions: AndroidOptions(encryptedSharedPreferences: true), ); void main() async { // Avoid errors caused by flutter upgrade. // Importing 'package:flutter/widgets.dart' is required. WidgetsFlutterBinding.ensureInitialized(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); if (Platform.isWindows || Platform.isLinux) { // Initialize FFI sqfliteFfiInit(); } Logger.root.level = Level.FINER; // defaults to Level.INFO Logger.root.onRecord.listen((record) { // ignore: avoid_print print('${record.level.name}: ${record.time}: ${record.message}'); }); log.info('Initialize main'); log.info('Setup bloc oberserver'); Bloc.observer = GenericBlocObserver(); Directory appDataDir = await getApplicationDocumentsDirectory(); log.info('Run app'); runApp( App(appDataDir: appDataDir.path), ); } class App extends StatefulWidget { final String appDataDir; const App({ required this.appDataDir, super.key, }); @override State createState() => _AppState(); } class _AppState extends State { late final DatabaseController dbController; @override void initState() { super.initState(); dbController = DatabaseController(path.join(widget.appDataDir, 'data.db')); } @override void dispose() { dbController.close(); super.dispose(); } @override Widget build(BuildContext context) { final DatabaseController dbController = DatabaseController( path.join(widget.appDataDir, 'data.db'), ); return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => LoginCubit(), ), BlocProvider( create: (BuildContext context) => TodoFileCubit( repository: context.read(), localPath: widget.appDataDir, ), ), BlocProvider( create: (BuildContext context) => DrawerCubit(), ), // Default filter BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), ), ), BlocProvider( create: (BuildContext context) => FilterListBloc( repository: context.read(), ), ), BlocProvider( create: (BuildContext context) => InteractionSettingsCubit( repository: context.read(), ), ), ], child: Builder( builder: (BuildContext context) { return BlocBuilder( builder: (BuildContext context, LoginState state) { if (state is LoginLocal || state is LoginWebDAV) { return CoreApp(loginState: state); } else { return const InitialApp(); } }, ); }, ), ), ); } } class InitialApp extends StatelessWidget { final ThemeMode? themeMode; const InitialApp({ this.themeMode, super.key, }); Future _initialize(BuildContext context) async { if (context.mounted) { await context.read().load(); } if (context.mounted) { await context.read().load(); } if (context.mounted) { await context.read().load(); } if (context.mounted) { context.read() ..add(const FilterListSubscriped()) ..add(const FilterListSynchronizationRequested()); } if (context.mounted) { await context.read().login(); } return true; } @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: lightTheme, darkTheme: darkTheme, themeMode: themeMode, localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: [ Locale('en'), Locale('de'), ], home: MultiBlocListener( listeners: [ BlocListener( listener: (BuildContext context, LoginState state) { if (state is LoginError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, TodoFileState state) { if (state is TodoFileError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, FilterListState state) { if (state is FilterListError) { SnackBarHandler.error(context, state.message); } }, ), BlocListener( listener: (BuildContext context, FilterState state) { if (state is FilterError) { SnackBarHandler.error(context, state.message); } }, ), ], child: FutureBuilder( future: _initialize(context), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return BlocBuilder( builder: (BuildContext context, LoginState state) { if (state is LoginLoading || state is LoginLocal || state is LoginWebDAV) { // Keep loading screen to prevent screen flickering. return _loadingScreen(); } else { return const IntroPage(); } }, ); } else if (snapshot.hasError) { return _errorScreen(); } else { return _loadingScreen(); } }, ), ), ); } Widget _loadingScreen() { return Scaffold( body: Center( child: FractionallySizedBox( widthFactor: 0.5, heightFactor: 0.5, child: Image.asset('assets/icon/icon.png'), ), ), ); } Widget _errorScreen() { return const Scaffold( body: Center( child: Text('Something went wrong.'), ), ); } } class CoreApp extends StatelessWidget { final LoginState loginState; final ThemeMode? themeMode; const CoreApp({ required this.loginState, this.themeMode, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoFileState todoFileState) { final TodoListRepository todoListRepository = _createTodoListRepository(loginState, todoFileState); final TodoListBloc todoListBloc = TodoListBloc( repository: todoListRepository, ) ..add(const TodoListSubscriptionRequested()) ..add(const TodoListSynchronizationRequested()); return RepositoryProvider( create: (BuildContext context) => todoListRepository, child: BlocProvider.value( value: todoListBloc, child: Builder( builder: (BuildContext context) { return MaterialApp.router( title: 'ntodotxt', debugShowCheckedModeBanner: false, theme: lightTheme, darkTheme: darkTheme, themeMode: themeMode, localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: [ Locale('en'), Locale('de'), ], routerConfig: AppRouter().config, ); }, ), ), ); }, ); } TodoListRepository _createTodoListRepository( LoginState loginState, TodoFileState todoFileState) { late TodoListApi api; switch (loginState) { case LoginLocal(): log.info('Use local backend'); log.info('Using local todo file ${todoFileState.localTodoFilePath}'); api = LocalTodoListApi.fromString( localFilePath: todoFileState.localTodoFilePath, ); case LoginWebDAV(): log.info('Use local+webdav backend'); log.info('Using remote todo file ${todoFileState.localTodoFilePath}'); api = WebDAVTodoListApi.fromString( localFilePath: todoFileState.localTodoFilePath, remoteFilePath: todoFileState.remoteTodoFilePath, client: WebDAVClient( server: loginState.server, path: loginState.path, username: loginState.username, password: loginState.password, acceptUntrustedCert: loginState.acceptUntrustedCert, ), ); default: log.info('Fallback to local backend'); log.info('Using local todo file ${todoFileState.localTodoFilePath}'); api = LocalTodoListApi.fromString( localFilePath: todoFileState.localTodoFilePath, ); } return TodoListRepository(api); } } ================================================ FILE: lib/oss_licenses.dart ================================================ // coverage:ignore-file // cSpell:disable // ignore_for_file: always_put_required_named_parameters_first // ignore_for_file: constant_identifier_names // ignore_for_file: sort_constructors_first // This code was generated by flutter_oss_licenses // https://pub.dev/packages/flutter_oss_licenses /// All dependencies including transitive dependencies. const allDependencies = [ _archive, _args, _async, _bloc, _boolean_selector, _characters, _checked_yaml, _cli_util, _clock, _collection, _convert, _cross_file, _crypto, _cupertino_icons, _dart_pubspec_licenses, _dio, _dio_web_adapter, _equatable, _fake_async, _ffi, _file, _file_picker, _flex_color_scheme, _flex_seed_scheme, _flutter, _flutter_bloc, _flutter_launcher_icons, _flutter_lints, _flutter_oss_licenses, _flutter_plugin_android_lifecycle, _flutter_secure_storage, _flutter_secure_storage_linux, _flutter_secure_storage_macos, _flutter_secure_storage_platform_interface, _flutter_secure_storage_web, _flutter_secure_storage_windows, _go_router, _http_parser, _image, _intl, _js, _json_annotation, _leak_tracker, _leak_tracker_flutter_testing, _leak_tracker_testing, _lints, _logging, _matcher, _material_color_utilities, _meta, _mime, _nested, _path, _path_provider, _path_provider_android, _path_provider_foundation, _path_provider_linux, _path_provider_platform_interface, _path_provider_windows, _permission_handler, _permission_handler_android, _permission_handler_apple, _permission_handler_html, _permission_handler_platform_interface, _permission_handler_windows, _petitparser, _platform, _plugin_platform_interface, _posix, _process, _provider, _rxdart, _source_span, _sqflite_common, _sqflite_common_ffi, _sqlite3, _sqlite3_flutter_libs, _stack_trace, _stream_channel, _string_scanner, _sync_http, _synchronized, _term_glyph, _test_api, _test_cov_console, _typed_data, _url_launcher, _url_launcher_android, _url_launcher_ios, _url_launcher_linux, _url_launcher_macos, _url_launcher_platform_interface, _url_launcher_web, _url_launcher_windows, _vector_math, _vm_service, _web, _webdav_client, _webdriver, _win32, _xdg_directories, _xml, _yaml ]; /// Direct `dependencies`. const dependencies = [ _collection, _crypto, _cupertino_icons, _dio, _equatable, _file_picker, _flex_color_scheme, _flutter, _flutter_bloc, _flutter_secure_storage, _go_router, _intl, _logging, _path, _path_provider, _permission_handler, _rxdart, _sqflite_common_ffi, _sqlite3_flutter_libs, _url_launcher, _webdav_client ]; /// Direct `dev_dependencies`. const devDependencies = [ _file, _flutter_launcher_icons, _flutter_lints, _flutter_oss_licenses, _test_cov_console ]; /// Package license definition. class Package { /// Package name final String name; /// Description final String description; /// Website URL final String? homepage; /// Repository URL final String? repository; /// Authors final List authors; /// Version final String version; /// License final String? license; /// Whether the license is in markdown format or not (plain text). final bool isMarkdown; /// Whether the package is included in the SDK or not. final bool isSdk; /// Direct dependencies final List dependencies; const Package({ required this.name, required this.description, this.homepage, this.repository, required this.authors, required this.version, this.license, required this.isMarkdown, required this.isSdk, required this.dependencies, }); } class PackageRef { final String name; const PackageRef(this.name); Package resolve() => allDependencies.firstWhere((d) => d.name == name); } /// archive 4.0.7 const _archive = Package( name: 'archive', description: 'Provides encoders and decoders for various archive and compression formats such as zip, tar, bzip2, gzip, and zlib.', repository: 'https://github.com/brendan-duncan/archive', authors: [], version: '4.0.7', license: '''The MIT License Copyright (c) 2013-2021 Brendan Duncan. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('crypto'), PackageRef('path'), PackageRef('posix') ]); /// args 2.7.0 const _args = Package( name: 'args', description: 'Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/args', authors: [], version: '2.7.0', license: '''Copyright 2013, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// async 2.11.0 const _async = Package( name: 'async', description: "Utility functions and classes related to the 'dart:async' library.", repository: 'https://github.com/dart-lang/async', authors: [], version: '2.11.0', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection'), PackageRef('meta')]); /// bloc 9.0.0 const _bloc = Package( name: 'bloc', description: 'A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern.', homepage: 'https://github.com/felangel/bloc', repository: 'https://github.com/felangel/bloc/tree/master/packages/bloc', authors: [], version: '9.0.0', license: '''The MIT License (MIT) Copyright (c) 2024 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta')]); /// boolean_selector 2.1.1 const _boolean_selector = Package( name: 'boolean_selector', description: "A flexible syntax for boolean expressions, based on a simplified version of Dart's expression syntax.", repository: 'https://github.com/dart-lang/boolean_selector', authors: [], version: '2.1.1', license: '''Copyright 2016, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// characters 1.3.0 const _characters = Package( name: 'characters', description: 'String replacement with operations that are Unicode/grapheme cluster aware.', repository: 'https://github.com/dart-lang/characters', authors: [], version: '1.3.0', license: '''Copyright 2019, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// checked_yaml 2.0.3 const _checked_yaml = Package( name: 'checked_yaml', description: 'Generate more helpful exceptions when decoding YAML documents using package:json_serializable and package:yaml.', repository: 'https://github.com/google/json_serializable.dart/tree/master/checked_yaml', authors: [], version: '2.0.3', license: '''Copyright 2019, the Dart project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('json_annotation'), PackageRef('source_span'), PackageRef('yaml') ]); /// cli_util 0.4.2 const _cli_util = Package( name: 'cli_util', description: 'A library to help in building Dart command-line apps.', repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/cli_util', authors: [], version: '0.4.2', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta'), PackageRef('path')]); /// clock 1.1.1 const _clock = Package( name: 'clock', description: 'A fakeable wrapper for dart:core clock APIs.', repository: 'https://github.com/dart-lang/clock', authors: [], version: '1.1.1', license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: []); /// collection 1.18.0 const _collection = Package( name: 'collection', description: 'Collections and utilities functions and classes related to collections.', repository: 'https://github.com/dart-lang/collection', authors: [], version: '1.18.0', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// convert 3.1.2 const _convert = Package( name: 'convert', description: 'Utilities for converting between data representations. Provides a number of Sink, Codec, Decoder, and Encoder types.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/convert', authors: [], version: '3.1.2', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('typed_data')]); /// cross_file 0.3.4+2 const _cross_file = Package( name: 'cross_file', description: 'An abstraction to allow working with files across multiple platforms.', repository: 'https://github.com/flutter/packages/tree/main/packages/cross_file', authors: [], version: '0.3.4+2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta'), PackageRef('web')]); /// crypto 3.0.6 const _crypto = Package( name: 'crypto', description: 'Implementations of SHA, MD5, and HMAC cryptographic functions.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/crypto', authors: [], version: '3.0.6', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('typed_data')]); /// cupertino_icons 1.0.8 const _cupertino_icons = Package( name: 'cupertino_icons', description: 'Default icons asset for Cupertino widgets based on Apple styled icons', repository: 'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons', authors: [], version: '1.0.8', license: '''The MIT License (MIT) Copyright (c) 2016 Vladimir Kharlampidi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: []); /// dart_pubspec_licenses 3.0.4 const _dart_pubspec_licenses = Package( name: 'dart_pubspec_licenses', description: 'A library to make it easy to extract OSS license information from Dart packages using pubspec.yaml', homepage: 'https://github.com/espresso3389/flutter_oss_licenses/tree/master/packages/dart_pubspec_licenses', repository: 'https://github.com/espresso3389/flutter_oss_licenses', authors: [], version: '3.0.4', license: '''MIT License Copyright (c) 2019 Takashi Kawasaki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('yaml'), PackageRef('path'), PackageRef('json_annotation') ]); /// dio 5.9.0 const _dio = Package( name: 'dio', description: '''A powerful HTTP networking package, supports Interceptors, Aborting and canceling a request, Custom adapters, Transformers, etc. ''', homepage: 'https://github.com/cfug/dio', repository: 'https://github.com/cfug/dio/blob/main/dio', authors: [], version: '5.9.0', license: '''MIT License Copyright (c) 2018 Wen Du (wendux) Copyright (c) 2022 The CFUG Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('async'), PackageRef('collection'), PackageRef('http_parser'), PackageRef('meta'), PackageRef('mime'), PackageRef('path'), PackageRef('dio_web_adapter') ]); /// dio_web_adapter 2.1.1 const _dio_web_adapter = Package( name: 'dio_web_adapter', description: 'An adapter that supports Dio on Web.', homepage: 'https://github.com/cfug/dio', repository: 'https://github.com/cfug/dio/blob/main/plugins/web_adapter', authors: [], version: '2.1.1', license: '''MIT License Copyright (c) 2018 Wen Du (wendux) Copyright (c) 2022 The CFUG Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('dio'), PackageRef('http_parser'), PackageRef('meta'), PackageRef('web') ]); /// equatable 2.0.7 const _equatable = Package( name: 'equatable', description: 'A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.', homepage: 'https://github.com/felangel/equatable', repository: 'https://github.com/felangel/equatable', authors: [], version: '2.0.7', license: '''MIT License Copyright (c) 2024 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection'), PackageRef('meta')]); /// fake_async 1.3.1 const _fake_async = Package( name: 'fake_async', description: 'Fake asynchronous events such as timers and microtasks for deterministic testing.', repository: 'https://github.com/dart-lang/fake_async', authors: [], version: '1.3.1', license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: []); /// ffi 2.1.3 const _ffi = Package( name: 'ffi', description: 'Utilities for working with Foreign Function Interface (FFI) code.', repository: 'https://github.com/dart-lang/native/tree/main/pkgs/ffi', authors: [], version: '2.1.3', license: '''Copyright 2019, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// file 7.0.0 const _file = Package( name: 'file', description: '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.', repository: 'https://github.com/google/file.dart/tree/master/packages/file', authors: [], version: '7.0.0', license: '''Copyright 2017, the Dart project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta'), PackageRef('path')]); /// file_picker 10.1.9 const _file_picker = Package( name: 'file_picker', description: 'A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.', homepage: 'https://github.com/miguelpruivo/plugins_flutter_file_picker', repository: 'https://github.com/miguelpruivo/flutter_file_picker', authors: [], version: '10.1.9', license: '''MIT License Copyright (c) 2018 Miguel Ruivo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('flutter_plugin_android_lifecycle'), PackageRef('plugin_platform_interface'), PackageRef('ffi'), PackageRef('path'), PackageRef('win32'), PackageRef('cross_file'), PackageRef('web') ]); /// flex_color_scheme 8.0.2 const _flex_color_scheme = Package( name: 'flex_color_scheme', description: 'A Flutter package to use and make beautiful Material design based themes.', homepage: 'https://docs.flexcolorscheme.com', repository: 'https://github.com/rydmike/flex_color_scheme', authors: [], version: '8.0.2', license: '''BSD 3-Clause License FlexColorScheme Copyright (c) 2020 - 2024 Mike Rydstrom (Twitter @RydMike GitHub rydmike) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flex_seed_scheme'), PackageRef('flutter'), PackageRef('meta') ]); /// flex_seed_scheme 3.4.1 const _flex_seed_scheme = Package( name: 'flex_seed_scheme', description: "A more flexible and powerful version of Flutter's ColorScheme.fromSeed. Use multiple seed colors, custom chroma and tone mapping.", homepage: 'https://github.com/rydmike/flex_seed_scheme', repository: 'https://github.com/rydmike/flex_seed_scheme', authors: [], version: '3.4.1', license: '''BSD 3-Clause License FlexSeedScheme Copyright (c) 2022-2024 Mike Rydstrom (Twitter @RydMike GitHub rydmike) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('flutter'), PackageRef('meta') ]); /// flutter 3.24.5 const _flutter = Package( name: 'flutter', description: 'A framework for writing Flutter applications', homepage: 'https://flutter.dev', authors: [], version: '3.24.5', license: '''Copyright 2014 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: true, dependencies: [ PackageRef('characters'), PackageRef('collection'), PackageRef('material_color_utilities'), PackageRef('meta'), PackageRef('vector_math') ]); /// flutter_bloc 9.1.1 const _flutter_bloc = Package( name: 'flutter_bloc', description: '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.', homepage: 'https://bloclibrary.dev', repository: 'https://github.com/felangel/bloc/tree/master/packages/flutter_bloc', authors: [], version: '9.1.1', license: '''The MIT License (MIT) Copyright (c) 2025 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('bloc'), PackageRef('flutter'), PackageRef('provider') ]); /// flutter_launcher_icons 0.14.3 const _flutter_launcher_icons = Package( name: 'flutter_launcher_icons', description: "A package which simplifies the task of updating your Flutter app's launcher icon.", homepage: 'https://github.com/fluttercommunity/flutter_launcher_icons', repository: 'https://github.com/fluttercommunity/flutter_launcher_icons/', authors: [], version: '0.14.3', license: '''MIT License Copyright (c) 2019 Mark O'Sullivan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('args'), PackageRef('checked_yaml'), PackageRef('cli_util'), PackageRef('image'), PackageRef('json_annotation'), PackageRef('path'), PackageRef('yaml') ]); /// flutter_lints 5.0.0 const _flutter_lints = Package( name: 'flutter_lints', description: 'Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.', repository: 'https://github.com/flutter/packages/tree/main/packages/flutter_lints', authors: [], version: '5.0.0', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('lints')]); /// flutter_oss_licenses 3.0.4 const _flutter_oss_licenses = Package( name: 'flutter_oss_licenses', description: 'A tool to generate detail and better OSS license list using pubspec.yaml/lock files.', homepage: 'https://github.com/espresso3389/flutter_oss_licenses/tree/master/packages/flutter_oss_licenses', repository: 'https://github.com/espresso3389/flutter_oss_licenses', authors: [], version: '3.0.4', license: '''MIT License Copyright (c) 2019 Takashi Kawasaki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('path'), PackageRef('meta'), PackageRef('yaml'), PackageRef('dart_pubspec_licenses'), PackageRef('args') ]); /// flutter_plugin_android_lifecycle 2.0.26 const _flutter_plugin_android_lifecycle = Package( name: 'flutter_plugin_android_lifecycle', description: 'Flutter plugin for accessing an Android Lifecycle within other plugins.', repository: 'https://github.com/flutter/packages/tree/main/packages/flutter_plugin_android_lifecycle', authors: [], version: '2.0.26', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('flutter')]); /// flutter_secure_storage 9.2.4 const _flutter_secure_storage = Package( name: 'flutter_secure_storage', description: 'Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.', repository: 'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage', authors: [], version: '9.2.4', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('flutter_secure_storage_linux'), PackageRef('flutter_secure_storage_macos'), PackageRef('flutter_secure_storage_platform_interface'), PackageRef('flutter_secure_storage_web'), PackageRef('flutter_secure_storage_windows'), PackageRef('meta') ]); /// flutter_secure_storage_linux 1.2.3 const _flutter_secure_storage_linux = Package( name: 'flutter_secure_storage_linux', description: 'Linux implementation of flutter_secure_storage', repository: 'https://github.com/mogol/flutter_secure_storage', authors: [], version: '1.2.3', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('flutter_secure_storage_platform_interface') ]); /// flutter_secure_storage_macos 3.1.3 const _flutter_secure_storage_macos = Package( name: 'flutter_secure_storage_macos', description: 'macOS implementation of flutter_secure_storage', repository: 'https://github.com/mogol/flutter_secure_storage', authors: [], version: '3.1.3', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('flutter_secure_storage_platform_interface') ]); /// flutter_secure_storage_platform_interface 1.1.2 const _flutter_secure_storage_platform_interface = Package( name: 'flutter_secure_storage_platform_interface', description: 'A common platform interface for the flutter_secure_storage plugin.', homepage: 'https://github.com/mogol/flutter_secure_storage', authors: [], version: '1.1.2', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('plugin_platform_interface') ]); /// flutter_secure_storage_web 1.2.1 const _flutter_secure_storage_web = Package( name: 'flutter_secure_storage_web', description: 'Web implementation of flutter_secure_storage. Use flutter_secure_storage for the full flutter package.', repository: 'https://github.com/mogol/flutter_secure_storage', authors: [], version: '1.2.1', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('flutter_secure_storage_platform_interface'), PackageRef('js') ]); /// flutter_secure_storage_windows 3.1.2 const _flutter_secure_storage_windows = Package( name: 'flutter_secure_storage_windows', description: 'Windows implementation of flutter_secure_storage. Please use flutter_secure_storage instead of this package.', repository: 'https://github.com/mogol/flutter_secure_storage', authors: [], version: '3.1.2', license: '''BSD 3-Clause License Copyright 2017 German Saprykin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('ffi'), PackageRef('flutter'), PackageRef('flutter_secure_storage_platform_interface'), PackageRef('path'), PackageRef('path_provider'), PackageRef('win32') ]); /// go_router 15.1.2 const _go_router = Package( name: 'go_router', description: 'A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more', repository: 'https://github.com/flutter/packages/tree/main/packages/go_router', authors: [], version: '15.1.2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('flutter'), PackageRef('logging'), PackageRef('meta') ]); /// http_parser 4.0.2 const _http_parser = Package( name: 'http_parser', description: 'A platform-independent package for parsing and serializing HTTP formats.', repository: 'https://github.com/dart-lang/http_parser', authors: [], version: '4.0.2', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('source_span'), PackageRef('string_scanner'), PackageRef('typed_data') ]); /// image 4.5.4 const _image = Package( name: 'image', description: 'Dart Image Library provides server and web apps the ability to load, manipulate, and save images with various image file formats.', homepage: 'https://github.com/brendan-duncan/image', authors: [], version: '4.5.4', license: '''The MIT License Copyright (c) 2013-2022 Brendan Duncan. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('archive'), PackageRef('meta'), PackageRef('xml') ]); /// intl 0.19.0 const _intl = Package( name: 'intl', description: 'Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.', repository: 'https://github.com/dart-lang/i18n/tree/main/pkgs/intl', authors: [], version: '0.19.0', license: '''Copyright 2013, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('clock'), PackageRef('meta'), PackageRef('path') ]); /// js 0.6.7 const _js = Package( name: 'js', description: 'Annotations to create static Dart interfaces for JavaScript APIs.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/js', authors: [], version: '0.6.7', license: '''Copyright 2012, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta')]); /// json_annotation 4.9.0 const _json_annotation = Package( name: 'json_annotation', description: 'Classes and helper functions that support JSON code generation via the `json_serializable` package.', repository: 'https://github.com/google/json_serializable.dart/tree/master/json_annotation', authors: [], version: '4.9.0', license: '''Copyright 2017, the Dart project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta')]); /// leak_tracker 10.0.5 const _leak_tracker = Package( name: 'leak_tracker', description: 'A framework for memory leak tracking for Dart and Flutter applications.', repository: 'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker', authors: [], version: '10.0.5', license: '''Copyright 2022, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// leak_tracker_flutter_testing 3.0.5 const _leak_tracker_flutter_testing = Package( name: 'leak_tracker_flutter_testing', description: 'An internal package to test leak tracking with Flutter.', repository: 'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_flutter_testing', authors: [], version: '3.0.5', license: '''Copyright 2022, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// leak_tracker_testing 3.0.1 const _leak_tracker_testing = Package( name: 'leak_tracker_testing', description: 'Leak tracking code intended for usage in tests.', repository: 'https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_testing', authors: [], version: '3.0.1', license: '''Copyright 2022, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// lints 5.0.0 const _lints = Package( name: 'lints', description: """Official Dart lint rules. Defines the 'core' and 'recommended' set of lints suggested by the Dart team. """, repository: 'https://github.com/dart-lang/lints', authors: [], version: '5.0.0', license: '''Copyright 2021, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// logging 1.3.0 const _logging = Package( name: 'logging', description: 'Provides APIs for debugging and error logging, similar to loggers in other languages, such as the Closure JS Logger and java.util.logging.Logger.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/logging', authors: [], version: '1.3.0', license: '''Copyright 2013, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// matcher 0.12.16+1 const _matcher = Package( name: 'matcher', description: 'Support for specifying test expectations via an extensible Matcher class. Also includes a number of built-in Matcher implementations for common cases.', repository: 'https://github.com/dart-lang/matcher', authors: [], version: '0.12.16+1', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// material_color_utilities 0.11.1 const _material_color_utilities = Package( name: 'material_color_utilities', description: '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.', repository: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', authors: [], version: '0.11.1', license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection')]); /// meta 1.15.0 const _meta = Package( name: 'meta', description: "Annotations used to express developer intentions that can't otherwise be deduced by statically analyzing source code.", repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/meta', authors: [], version: '1.15.0', license: '''Copyright 2016, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// mime 2.0.0 const _mime = Package( name: 'mime', description: 'Utilities for handling media (MIME) types, including determining a type from a file extension and file contents.', repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/mime', authors: [], version: '2.0.0', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// nested 1.0.0 const _nested = Package( name: 'nested', description: 'A Flutter Widget which helps nest multiple widgets without needing to manually nest them.', repository: 'https://github.com/rrousselGit/nested', authors: [], version: '1.0.0', license: '''MIT License Copyright (c) 2019 Remi Rousselet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('flutter')]); /// path 1.9.0 const _path = Package( name: 'path', description: '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.', repository: 'https://github.com/dart-lang/path', authors: [], version: '1.9.0', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// path_provider 2.1.5 const _path_provider = Package( name: 'path_provider', description: 'Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider', authors: [], version: '2.1.5', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('path_provider_android'), PackageRef('path_provider_foundation'), PackageRef('path_provider_linux'), PackageRef('path_provider_platform_interface'), PackageRef('path_provider_windows') ]); /// path_provider_android 2.2.15 const _path_provider_android = Package( name: 'path_provider_android', description: 'Android implementation of the path_provider plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_android', authors: [], version: '2.2.15', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('path_provider_platform_interface') ]); /// path_provider_foundation 2.4.1 const _path_provider_foundation = Package( name: 'path_provider_foundation', description: 'iOS and macOS implementation of the path_provider plugin', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_foundation', authors: [], version: '2.4.1', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('path_provider_platform_interface') ]); /// path_provider_linux 2.2.1 const _path_provider_linux = Package( name: 'path_provider_linux', description: 'Linux implementation of the path_provider plugin', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_linux', authors: [], version: '2.2.1', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('ffi'), PackageRef('flutter'), PackageRef('path'), PackageRef('path_provider_platform_interface'), PackageRef('xdg_directories') ]); /// path_provider_platform_interface 2.1.2 const _path_provider_platform_interface = Package( name: 'path_provider_platform_interface', description: 'A common platform interface for the path_provider plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_platform_interface', authors: [], version: '2.1.2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('platform'), PackageRef('plugin_platform_interface') ]); /// path_provider_windows 2.3.0 const _path_provider_windows = Package( name: 'path_provider_windows', description: 'Windows implementation of the path_provider plugin', repository: 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_windows', authors: [], version: '2.3.0', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('ffi'), PackageRef('flutter'), PackageRef('path'), PackageRef('path_provider_platform_interface') ]); /// permission_handler 12.0.0+1 const _permission_handler = Package( name: 'permission_handler', description: 'Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.', repository: 'https://github.com/baseflow/flutter-permission-handler', authors: [], version: '12.0.0+1', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('meta'), PackageRef('permission_handler_android'), PackageRef('permission_handler_apple'), PackageRef('permission_handler_html'), PackageRef('permission_handler_windows'), PackageRef('permission_handler_platform_interface') ]); /// permission_handler_android 13.0.1 const _permission_handler_android = Package( name: 'permission_handler_android', description: 'Permission plugin for Flutter. This plugin provides the Android API to request and check permissions.', homepage: 'https://github.com/baseflow/flutter-permission-handler', authors: [], version: '13.0.1', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('permission_handler_platform_interface') ]); /// permission_handler_apple 9.4.7 const _permission_handler_apple = Package( name: 'permission_handler_apple', description: 'Permission plugin for Flutter. This plugin provides the iOS API to request and check permissions.', repository: 'https://github.com/baseflow/flutter-permission-handler', authors: [], version: '9.4.7', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('permission_handler_platform_interface') ]); /// permission_handler_html 0.1.3+5 const _permission_handler_html = Package( name: 'permission_handler_html', description: 'Permission plugin for Flutter. This plugin provides the web API to request and check permissions.', homepage: 'https://github.com/baseflow/flutter-permission-handler', authors: [], version: '0.1.3+5', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('permission_handler_platform_interface'), PackageRef('web') ]); /// permission_handler_platform_interface 4.3.0 const _permission_handler_platform_interface = Package( name: 'permission_handler_platform_interface', description: 'A common platform interface for the permission_handler plugin.', homepage: 'https://github.com/baseflow/flutter-permission-handler/tree/master/permission_handler_platform_interface', authors: [], version: '4.3.0', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('meta'), PackageRef('plugin_platform_interface') ]); /// permission_handler_windows 0.2.1 const _permission_handler_windows = Package( name: 'permission_handler_windows', description: 'Permission plugin for Flutter. This plugin provides the Windows API to request and check permissions.', homepage: 'https://github.com/baseflow/flutter-permission-handler', authors: [], version: '0.2.1', license: '''MIT License Copyright (c) 2018 Baseflow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('permission_handler_platform_interface') ]); /// petitparser 6.0.2 const _petitparser = Package( name: 'petitparser', description: 'A dynamic parser framework to build efficient grammars and parsers quickly.', homepage: 'https://petitparser.github.io', repository: 'https://github.com/petitparser/dart-petitparser', authors: [], version: '6.0.2', license: '''The MIT License Copyright (c) 2006-2023 Lukas Renggli. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta')]); /// platform 3.1.5 const _platform = Package( name: 'platform', description: 'A pluggable, mockable platform information abstraction for Dart.', repository: 'https://github.com/dart-lang/platform', authors: [], version: '3.1.5', license: '''Copyright 2017, the Dart project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// plugin_platform_interface 2.1.8 const _plugin_platform_interface = Package( name: 'plugin_platform_interface', description: 'Reusable base class for platform interfaces of Flutter federated plugins, to help enforce best practices.', repository: 'https://github.com/flutter/packages/tree/main/packages/plugin_platform_interface', authors: [], version: '2.1.8', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta')]); /// posix 6.0.2 const _posix = Package( name: 'posix', description: 'Exposes the POSIX api on OSx and Linux', homepage: 'https://github.com/onepub-dev/dart_posix', authors: [], version: '6.0.2', license: '''MIT License Copyright (c) 2020 Brett Sutton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('ffi'), PackageRef('meta'), PackageRef('path')]); /// process 5.0.2 const _process = Package( name: 'process', description: 'A pluggable, mockable process invocation abstraction for Dart.', repository: 'https://github.com/flutter/packages/tree/main/packages/process', authors: [], version: '5.0.2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// provider 6.1.5 const _provider = Package( name: 'provider', description: 'A wrapper around InheritedWidget to make them easier to use and more reusable.', repository: 'https://github.com/rrousselGit/provider', authors: [], version: '6.1.5', license: '''MIT License Copyright (c) 2019 Remi Rousselet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('flutter'), PackageRef('nested') ]); /// rxdart 0.28.0 const _rxdart = Package( name: 'rxdart', description: '''RxDart is an implementation of the popular ReactiveX api for asynchronous programming, leveraging the native Dart Streams api. ''', repository: 'https://github.com/ReactiveX/rxdart', authors: [], version: '0.28.0', license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: []); /// source_span 1.10.0 const _source_span = Package( name: 'source_span', description: 'Provides a standard representation for source code locations and spans.', repository: 'https://github.com/dart-lang/source_span', authors: [], version: '1.10.0', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('path'), PackageRef('term_glyph') ]); /// sqflite_common 2.5.4+6 const _sqflite_common = Package( name: 'sqflite_common', description: 'Dart wrapper on SQLite, a self-contained, high-reliability, embedded, SQL database engine.', homepage: 'https://github.com/tekartik/sqflite/tree/master/sqflite_common', authors: [], version: '2.5.4+6', license: '''BSD 2-Clause License Copyright (c) 2019, Alexandre Roux Tekartik All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('synchronized'), PackageRef('path'), PackageRef('meta') ]); /// sqflite_common_ffi 2.3.4+4 const _sqflite_common_ffi = Package( name: 'sqflite_common_ffi', description: 'sqflite ffi based implementation, for desktop and units tests.', homepage: 'https://github.com/tekartik/sqflite/tree/master/sqflite_common_ffi', authors: [], version: '2.3.4+4', license: '''BSD 2-Clause License Copyright (c) 2019, Alexandre Roux Tekartik All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('sqlite3'), PackageRef('sqflite_common'), PackageRef('synchronized'), PackageRef('path'), PackageRef('meta') ]); /// sqlite3 2.7.5 const _sqlite3 = Package( name: 'sqlite3', description: 'Provides lightweight yet convenient bindings to SQLite by using dart:ffi', homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3', authors: [], version: '2.7.5', license: '''MIT License Copyright (c) 2020 Simon Binder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data') ]); /// sqlite3_flutter_libs 0.5.32 const _sqlite3_flutter_libs = Package( name: 'sqlite3_flutter_libs', description: 'Flutter plugin to include native sqlite3 libraries with your app', homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs', authors: [], version: '0.5.32', license: '''MIT License Copyright (c) 2020 Simon Binder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('flutter')]); /// stack_trace 1.11.1 const _stack_trace = Package( name: 'stack_trace', description: 'A package for manipulating stack traces and printing them readably.', repository: 'https://github.com/dart-lang/stack_trace', authors: [], version: '1.11.1', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// stream_channel 2.1.2 const _stream_channel = Package( name: 'stream_channel', description: 'An abstraction for two-way communication channels based on the Dart Stream class.', repository: 'https://github.com/dart-lang/stream_channel', authors: [], version: '2.1.2', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// string_scanner 1.2.0 const _string_scanner = Package( name: 'string_scanner', description: 'A class for parsing strings using a sequence of patterns.', repository: 'https://github.com/dart-lang/string_scanner', authors: [], version: '1.2.0', license: '''Copyright 2014, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('source_span')]); /// sync_http 0.3.1 const _sync_http = Package( name: 'sync_http', description: 'Synchronous HTTP client for Dart.', repository: 'https://github.com/google/sync_http.dart', authors: [], version: '0.3.1', license: '''Copyright 2017, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// synchronized 3.3.0+3 const _synchronized = Package( name: 'synchronized', description: 'Lock mechanism to prevent concurrent access to asynchronous code.', homepage: 'https://github.com/tekartik/synchronized.dart/tree/master/synchronized', authors: [], version: '3.3.0+3', license: '''MIT License Copyright (c) 2016, Alexandre Roux Tekartik. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: []); /// term_glyph 1.2.1 const _term_glyph = Package( name: 'term_glyph', description: 'Useful Unicode glyphs and ASCII substitutes.', repository: 'https://github.com/dart-lang/term_glyph', authors: [], version: '1.2.1', license: '''Copyright 2017, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// test_api 0.7.2 const _test_api = Package( name: 'test_api', description: 'The user facing API for structuring Dart tests and checking expectations.', repository: 'https://github.com/dart-lang/test/tree/master/pkgs/test_api', authors: [], version: '0.7.2', license: '''Copyright 2018, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// test_cov_console 0.2.2 const _test_cov_console = Package( name: 'test_cov_console', description: 'Command line utility to read lcov.info and print the coverage report to console.', homepage: 'https://github.com/DigitalKatalis/test_cov_console', authors: [], version: '0.2.2', license: '''Copyright (c) 2021, DKatalis. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// typed_data 1.4.0 const _typed_data = Package( name: 'typed_data', description: 'Utility functions and classes related to the dart:typed_data library.', repository: 'https://github.com/dart-lang/core/tree/main/pkgs/typed_data', authors: [], version: '1.4.0', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('collection')]); /// url_launcher 6.3.1 const _url_launcher = Package( name: 'url_launcher', description: 'Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher', authors: [], version: '6.3.1', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_android'), PackageRef('url_launcher_ios'), PackageRef('url_launcher_linux'), PackageRef('url_launcher_macos'), PackageRef('url_launcher_platform_interface'), PackageRef('url_launcher_web'), PackageRef('url_launcher_windows') ]); /// url_launcher_android 6.3.14 const _url_launcher_android = Package( name: 'url_launcher_android', description: 'Android implementation of the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android', authors: [], version: '6.3.14', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface') ]); /// url_launcher_ios 6.3.3 const _url_launcher_ios = Package( name: 'url_launcher_ios', description: 'iOS implementation of the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios', authors: [], version: '6.3.3', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface') ]); /// url_launcher_linux 3.2.1 const _url_launcher_linux = Package( name: 'url_launcher_linux', description: 'Linux implementation of the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_linux', authors: [], version: '3.2.1', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface') ]); /// url_launcher_macos 3.2.2 const _url_launcher_macos = Package( name: 'url_launcher_macos', description: 'macOS implementation of the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_macos', authors: [], version: '3.2.2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface') ]); /// url_launcher_platform_interface 2.3.2 const _url_launcher_platform_interface = Package( name: 'url_launcher_platform_interface', description: 'A common platform interface for the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_platform_interface', authors: [], version: '2.3.2', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('plugin_platform_interface') ]); /// url_launcher_web 2.3.3 const _url_launcher_web = Package( name: 'url_launcher_web', description: 'Web platform implementation of url_launcher', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_web', authors: [], version: '2.3.3', license: '''url_launcher_web Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- platform_detect Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 Workiva Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface'), PackageRef('web') ]); /// url_launcher_windows 3.1.4 const _url_launcher_windows = Package( name: 'url_launcher_windows', description: 'Windows implementation of the url_launcher plugin.', repository: 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows', authors: [], version: '3.1.4', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('flutter'), PackageRef('url_launcher_platform_interface') ]); /// vector_math 2.1.4 const _vector_math = Package( name: 'vector_math', description: 'A Vector Math library for 2D and 3D applications.', repository: 'https://github.com/google/vector_math.dart', authors: [], version: '2.1.4', license: '''Copyright 2015, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2013 Andrew Magill This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution.''', isMarkdown: false, isSdk: false, dependencies: []); /// vm_service 14.2.5 const _vm_service = Package( name: 'vm_service', description: 'A library to communicate with a service implementing the Dart VM service protocol.', repository: 'https://github.com/dart-lang/sdk/tree/main/pkg/vm_service', authors: [], version: '14.2.5', license: '''Copyright 2015, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// web 1.1.1 const _web = Package( name: 'web', description: 'Lightweight browser API bindings built around JS interop.', repository: 'https://github.com/dart-lang/web', authors: [], version: '1.1.1', license: '''Copyright 2023, the Dart project authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: []); /// webdav_client 1.2.2 const _webdav_client = Package( name: 'webdav_client', description: 'A simple WebDAV client that supports some common methods.', homepage: 'https://github.com/flymzero/webdav_client', authors: [], version: '1.2.2', license: '''BSD 3-Clause License Copyright (c) 2020, MZERO All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('dio'), PackageRef('xml'), PackageRef('convert') ]); /// webdriver 3.0.3 const _webdriver = Package( name: 'webdriver', description: 'Provides WebDriver bindings for Dart. Supports WebDriver JSON interface and W3C spec. Requires the use of WebDriver remote server.', repository: 'https://github.com/google/webdriver.dart', authors: [], version: '3.0.3', license: '''Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2013 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.''', isMarkdown: false, isSdk: false, dependencies: []); /// win32 5.10.1 const _win32 = Package( name: 'win32', description: '''Access common Win32 APIs directly from Dart using FFI — no C required! ''', homepage: 'https://win32.pub', repository: 'https://github.com/halildurmus/win32', authors: [], version: '5.10.1', license: '''BSD 3-Clause License Copyright (c) 2024, Halil Durmus Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('ffi')]); /// xdg_directories 1.1.0 const _xdg_directories = Package( name: 'xdg_directories', description: 'A Dart package for reading XDG directory configuration information on Linux.', repository: 'https://github.com/flutter/packages/tree/main/packages/xdg_directories', authors: [], version: '1.1.0', license: '''Copyright 2013 The Flutter Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', isMarkdown: false, isSdk: false, dependencies: [PackageRef('meta'), PackageRef('path')]); /// xml 6.5.0 const _xml = Package( name: 'xml', description: 'A lightweight library for parsing, traversing, querying, transforming and building XML documents.', homepage: 'https://github.com/renggli/dart-xml', authors: [], version: '6.5.0', license: '''The MIT License Copyright (c) 2006-2023 Lukas Renggli. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('meta'), PackageRef('petitparser') ]); /// yaml 3.1.3 const _yaml = Package( name: 'yaml', description: 'A parser for YAML, a human-friendly data serialization standard', repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/yaml', authors: [], version: '3.1.3', license: '''Copyright (c) 2014, the Dart project authors. Copyright (c) 2006, Kirill Simonov. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', isMarkdown: false, isSdk: false, dependencies: [ PackageRef('collection'), PackageRef('source_span'), PackageRef('string_scanner') ]); ================================================ FILE: lib/setting/controller/fake_setting_controller.dart ================================================ // coverage:ignore-file import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingControllerInterface; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; class FakeSettingController implements SettingControllerInterface { static final List settings = []; FakeSettingController(); @override Future> list() async => settings; @override Future get(dynamic identifier) async { for (Setting s in settings) { if (s.key == identifier) { return s; } } return null; } @override Future insert(Setting model) async { settings.add(model); return settings.length; } @override Future getOrInsert( {required dynamic identifier, required String defaultValue}) async { Setting? result = await get(identifier); if (result == null) { Setting fallback = Setting(key: identifier, value: defaultValue); await insert(fallback); return fallback; } else { return result; } } @override Future update(Setting model) async { int index = settings.indexWhere((Setting s) => s.key == model.key); if (index != -1) { settings[index] = model; return index; } else { return 0; } } @override Future updateOrInsert(Setting model) async { int id = await update(model); if (id == 0) { id = await insert(model); } return id; } @override Future delete(dynamic identifier) async { int index = settings.indexWhere((Setting s) => s.key == identifier); if (index != -1) { settings.removeAt(index); return index; } else { return 0; } } } ================================================ FILE: lib/setting/controller/setting_controller.dart ================================================ import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; abstract class SettingControllerInterface implements ModelControllerInterface { Future getOrInsert( {required dynamic identifier, required String defaultValue}); Future updateOrInsert(Setting model); } class SettingController implements SettingControllerInterface { final DatabaseController controller; SettingController(this.controller); @override Future> list() async { final Database db = await controller.database; final List> maps = await db.query('settings'); return List.generate(maps.length, (i) { return Setting.fromMap(maps[i]); }); } @override Future get(dynamic identifier) async { Setting? model; final Database db = await controller.database; final List maps = await db.query( 'settings', columns: ['key', 'value'], where: 'key = ?', whereArgs: [identifier as String], ); if (maps.isNotEmpty) { model = Setting.fromMap(maps.first); } return model; } @override Future insert(Setting model) async { final Database db = await controller.database; final int id = await db.insert( 'settings', model.toMap(), conflictAlgorithm: ConflictAlgorithm.ignore, ); return id; } @override Future getOrInsert( {required dynamic identifier, required String defaultValue}) async { Setting? result = await get(identifier); if (result == null) { Setting fallback = Setting(key: identifier, value: defaultValue); await insert(fallback); return fallback; } else { return result; } } @override Future update(Setting model) async { final Database db = await controller.database; final int id = await db.update( 'settings', model.toMap(), // Ensure that the model has a matching id. where: 'key = ?', // Pass the models id as a whereArg to prevent SQL injection. whereArgs: [model.key], ); return id; } @override Future updateOrInsert(Setting model) async { int id = await update(model); if (id == 0) { id = await insert(model); } return id; } @override Future delete(dynamic identifier) async { final Database db = await controller.database; final int id = await db.delete( 'settings', // Ensure that the model has a matching id. where: 'key = ?', // Pass the models id as a whereArg to prevent SQL injection. whereArgs: [identifier], ); return id; } } ================================================ FILE: lib/setting/model/setting_model.dart ================================================ import 'package:equatable/equatable.dart'; class Setting extends Equatable { final String key; final String value; const Setting({ required this.key, required this.value, }); Setting.fromMap(Map map) : key = map['key'] as String, value = map['value'] as String; static String get tableRepr { return '''CREATE TABLE IF NOT EXISTS settings( `id` INTEGER PRIMARY KEY, `key` TEXT NOT NULL UNIQUE, `value` TEXT NOT NULL )'''; } Setting copyWith({ String? key, String? value, }) { return Setting( key: key ?? this.key, value: value ?? this.value, ); } /// Convert a [Setting] into Map. /// The keys must correspond to the names of the /// columns in the database. Map toMap() { return { 'key': key, 'value': value, }; } @override String toString() => 'Setting { key: $key value: $value }'; @override List get props => [ key, value, ]; } ================================================ FILE: lib/setting/page/settings_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart' show PopScopeDrawer; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/common/widget/info_dialog.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/login/state/login_state.dart'; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/setting/state/interaction_settings_state.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context) { return const PopScopeDrawer( child: Scaffold( appBar: MainAppBar(title: 'Settings'), body: SettingsView(), ), ); } } class SettingsView extends StatelessWidget { const SettingsView({super.key}); @override Widget build(BuildContext context) { return ListView( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Filter', style: Theme.of(context).textTheme.titleSmall, ), ), ), const DefaultListOrderSettingsItem(), const DefaultListFilterSettingsItem(), const DefaultListGroupSettingsItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Interaction', style: Theme.of(context).textTheme.titleSmall, ), ), ), const SwipeLeftActionEnabledSettingsItem(), const SwipeRightActionEnabledSettingsItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Storage', style: Theme.of(context).textTheme.titleSmall, ), ), ), const TodoFilenameSettingsItem(), const LocalPathSettingsItem(), const RemotePathSettingsItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'App', style: Theme.of(context).textTheme.titleSmall, ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Reinitialization'), subtitle: const Text('Reinitialization of the app.'), onTap: () async { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Reinitialization', message: 'Do you want to reinitializate the app?', actionLabel: 'Reninitialize', cancelLabel: 'Cancel', ); if (context.mounted && confirm) { context.read().reset(); await context.read().resetTodoFileSettings(); if (context.mounted) { await context.read().logout(); } } }, ), ), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Others', style: Theme.of(context).textTheme.titleSmall, ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('About'), onTap: () => context.pushNamed('app-info'), ), ), ], ); } } class DefaultListOrderSettingsItem extends StatelessWidget { const DefaultListOrderSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) => previousState.filter.order != state.filter.order, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Default order'), subtitle: Text(state.filter.order.name), onTap: () async { await DefaultFilterStateOrderDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class DefaultListFilterSettingsItem extends StatelessWidget { const DefaultListFilterSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) => previousState.filter.filter != state.filter.filter, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Default filter'), subtitle: Text(state.filter.filter.name), onTap: () async { await DefaultFilterStateFilterDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class DefaultListGroupSettingsItem extends StatelessWidget { const DefaultListGroupSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState state) => previousState.filter.group != state.filter.group, builder: (BuildContext context, FilterState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Default grouping'), subtitle: Text(state.filter.group.name), onTap: () async { await DefaultFilterStateGroupDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ), ); }, ); } } class SwipeLeftActionEnabledSettingsItem extends StatelessWidget { const SwipeLeftActionEnabledSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (InteractionSettingsState previousState, InteractionSettingsState state) => previousState.swipeLeftActionEnabled != state.swipeLeftActionEnabled, builder: (BuildContext context, InteractionSettingsState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: CheckboxListTile( title: const Text('Enable swipe left action'), value: state.swipeLeftActionEnabled, subtitle: const Text( 'If enabled, you can swipe left on a item to perform an action.', ), isThreeLine: true, onChanged: (bool? value) { BlocProvider.of(context) .setSwipeLeftActionEnabled(value ?? false); }, ), ); }, ); } } class SwipeRightActionEnabledSettingsItem extends StatelessWidget { const SwipeRightActionEnabledSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (InteractionSettingsState previousState, InteractionSettingsState state) => previousState.swipeRightActionEnabled != state.swipeRightActionEnabled, builder: (BuildContext context, InteractionSettingsState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: CheckboxListTile( title: const Text('Enable swipe right action'), value: state.swipeRightActionEnabled, subtitle: const Text( 'If enabled, you can swipe right on a item to perform an action.', ), isThreeLine: true, onChanged: (bool? value) { BlocProvider.of(context) .setSwipeRightActionEnabled(value ?? false); }, ), ); }, ); } } class LocalPathSettingsItem extends StatelessWidget { const LocalPathSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoFileState previousState, TodoFileState state) => previousState.localPath != state.localPath, builder: (BuildContext context, TodoFileState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Local path'), subtitle: Text(state.localPath), onTap: () => InfoDialog.dialog( context: context, title: 'Local path', message: 'Changing this value after initializing the app is not supported.\n\nIf you want to change this value, you must reinitialize the app.', ), ), ); }, ); } } class RemotePathSettingsItem extends StatelessWidget { const RemotePathSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, LoginState loginState) { return Visibility( visible: loginState is LoginWebDAV, child: BlocBuilder( buildWhen: (TodoFileState previousTodoFileState, TodoFileState todoFileState) => previousTodoFileState.remotePath != todoFileState.remotePath, builder: (BuildContext context, TodoFileState todoFileState) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Remote path'), subtitle: Text(todoFileState.remotePath), onTap: () => InfoDialog.dialog( context: context, title: 'Remote path', message: 'Changing this value after initializing the app is not supported.\n\nIf you want to change this value, you must reinitialize the app.', ), ), ); }, ), ); }, ); } } class TodoFilenameSettingsItem extends StatelessWidget { const TodoFilenameSettingsItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoFileState previousState, TodoFileState state) => previousState.todoFilename != state.todoFilename, builder: (BuildContext context, TodoFileState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: const Text('Todo filename'), subtitle: Text(state.todoFilename), onTap: () => InfoDialog.dialog( context: context, title: 'Todo filename', message: 'Changing this value after initializing the app is not supported.\n\nIf you want to change this value, you must reinitialize the app.', ), ), ); }, ); } } ================================================ FILE: lib/setting/repository/setting_repository.dart ================================================ import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingControllerInterface; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; class SettingRepository { final SettingControllerInterface controller; SettingRepository(this.controller); Future> list() async => await controller.list(); Future get({required String key}) async => await controller.get(key); Future getOrInsert( {required String key, required String defaultValue}) async => await controller.getOrInsert(identifier: key, defaultValue: defaultValue); Future insert(Setting model) async => await controller.insert(model); Future update(Setting model) async => await controller.update(model); Future delete({required String key}) async => await controller.delete(key); Future updateOrInsert(Setting model) async => await controller.updateOrInsert(model); } ================================================ FILE: lib/setting/state/interaction_settings_cubit.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/setting/model/setting_model.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/setting/state/interaction_settings_state.dart'; class InteractionSettingsCubit extends Cubit { final SettingRepository repository; InteractionSettingsCubit({ required this.repository, }) : super( InteractionSettingsLoading( swipeLeftActionEnabled: false, swipeRightActionEnabled: false, ), ); bool parseBoolOrFalse(String? value) { return value?.trim().toLowerCase() == 'true'; } Future load() async { try { if (state is InteractionSettingsLoading) { emit( state.save( swipeLeftActionEnabled: parseBoolOrFalse( (await repository.get(key: 'swipeLeftActionEnabled'))?.value), swipeRightActionEnabled: parseBoolOrFalse( (await repository.get(key: 'swipeRightActionEnabled'))?.value), ), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future setSwipeLeftActionEnabled(bool value) async { try { emit(state.save(swipeLeftActionEnabled: value)); await repository.updateOrInsert( Setting(key: 'swipeLeftActionEnabled', value: value.toString()), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future setSwipeRightActionEnabled(bool value) async { try { emit(state.save(swipeRightActionEnabled: value)); await repository.updateOrInsert( Setting(key: 'swipeRightActionEnabled', value: value.toString()), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/setting/state/interaction_settings_state.dart ================================================ import 'package:equatable/equatable.dart'; sealed class InteractionSettingsState extends Equatable { final bool swipeLeftActionEnabled; final bool swipeRightActionEnabled; const InteractionSettingsState({ required this.swipeLeftActionEnabled, required this.swipeRightActionEnabled, }); InteractionSettingsLoading loading({ bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) { return InteractionSettingsLoading( swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled, ); } InteractionSettingsSaved save({ bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) { return InteractionSettingsSaved( swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled, ); } InteractionSettingsError error({ required String message, bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) { return InteractionSettingsError( message: message, swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled, ); } @override List get props => [ swipeLeftActionEnabled, swipeRightActionEnabled, ]; @override String toString() => 'InteractionSettingsState { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }'; } final class InteractionSettingsLoading extends InteractionSettingsState { const InteractionSettingsLoading({ required super.swipeLeftActionEnabled, required super.swipeRightActionEnabled, }); InteractionSettingsLoading copyWith({ bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) => super.loading( swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled, ); @override String toString() => 'InteractionSettingsLoading { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }'; } final class InteractionSettingsSaved extends InteractionSettingsState { const InteractionSettingsSaved({ required super.swipeLeftActionEnabled, required super.swipeRightActionEnabled, }); InteractionSettingsSaved copyWith({ bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) => super.save( swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled, ); @override String toString() => 'InteractionSettingsSaved { swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }'; } final class InteractionSettingsError extends InteractionSettingsState { final String message; const InteractionSettingsError({ required this.message, required super.swipeLeftActionEnabled, required super.swipeRightActionEnabled, }); InteractionSettingsError copyWith({ String? message, bool? swipeLeftActionEnabled, bool? swipeRightActionEnabled, }) => super.error( message: message ?? this.message, swipeLeftActionEnabled: swipeLeftActionEnabled ?? this.swipeLeftActionEnabled, swipeRightActionEnabled: swipeRightActionEnabled ?? this.swipeRightActionEnabled); @override List get props => [ message, swipeLeftActionEnabled, swipeRightActionEnabled, ]; @override String toString() => 'InteractionSettingsError { message: $message, swipeLeftActionEnabled: $swipeLeftActionEnabled, swipeRightActionEnabled: $swipeRightActionEnabled }'; } ================================================ FILE: lib/todo/api/todo_list_api.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:ntodotxt/main.dart' show log; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; import 'package:rxdart/subjects.dart'; import 'package:webdav_client/webdav_client.dart' as webdav; class LocalFile { final File file; LocalFile(String path) : file = File(path.replaceAllMapped(RegExp(r'\/{2,}'), (match) => '/')); LocalFile.fromFile(this.file); String get path => file.uri.pathSegments.last; Future get lastModified async => await file.lastModified(); } class WebDAVFile { final String path; final WebDAVClient client; WebDAVFile(String path, this.client) : path = path.replaceAllMapped(RegExp(r'\/{2,}'), (match) => '/'); Future get file async => await client.getFile(filename: path); Future get lastModified async => (await file).mTime; } abstract class TodoListApi { /// Provides a [Stream] of all todos read from the source. Stream> getTodoList(); Future initSource(); /// Read [todoList] from source. Future readFromSource(); /// Write [todoList] to source. Future writeToSource(); bool existsTodo(Todo todo); /// Saves a [todo]. /// If a [todo] with [id] already exists, it will be replaced. /// If the [todo] with [id] already exists it will be updated/merged. void saveTodo(Todo todo); /// Saves multiple [todos] by [id] at once. void saveMultipleTodos(List todos); /// Deletes the given [todo] by [id]. void deleteTodo(Todo todo); /// Deletes multiple [todos] by [id] at once. void deleteMultipleTodos(List todos); } class LocalTodoListApi extends TodoListApi { final LocalFile localFile; LocalTodoListApi(this.localFile) { if (localFile.file.existsSync() == false) { log.fine('File ${localFile.path} does not exist. Creating.'); localFile.file.createSync(); } else { log.fine('File ${localFile.path} exists already.'); } updateList(readSync()); // Read synchrone here. } LocalTodoListApi.fromString({ required String localFilePath, }) : this(LocalFile(localFilePath)); LocalTodoListApi.fromFile({ required File localFile, }) : this(LocalFile.fromFile(localFile)); /// Provides a [Stream] of all todos. // A special Streamcontroller that captures the latest item that has been // added to the controller, and emits that as the first item to any new listener. final BehaviorSubject> controller = BehaviorSubject>.seeded(const []); List get _todoList => controller.value; void updateList(List todoList) { // Update only if list does'nt match to prevent weird state changes. if (const ListEquality().equals(_todoList, todoList) == false) { log.fine('Update todo list.'); _dispatch(todoList); } else { log.fine('Skip update todo list. List matches with the previous one.'); } } void _dispatch(List todoList) { controller.add(todoList); log.finest( 'Updated todos ${[for (var todo in _todoList) todo]}', ); } void dispose() { controller.close(); } List _read(List rawTodoList) { return [ for (var t in rawTodoList) if (t.isNotEmpty) Todo.fromString(value: t) ]; } Future> read() async { log.info('Async-read todos from file'); return _read(await localFile.file.readAsLines()); } List readSync() { log.info('Sync-read todos from file'); return _read(localFile.file.readAsLinesSync()); } Future write(String content) async { log.info('Sync-write todos to file'); await localFile.file.writeAsString(content); } @override Stream> getTodoList() => controller.asBroadcastStream(); @override Future initSource() async { log.info('Initialize todo file'); if (await localFile.file.exists() == false) { await localFile.file.create(); } } @override Future readFromSource() async => updateList(await read()); @override Future writeToSource() async => write( _todoList.join(Platform.lineTerminator), ); @override bool existsTodo(Todo todo) => _todoList.indexWhere((t) => t.id == todo.id) == -1 ? false : true; List _save(List todoList, Todo todo) { int index = todoList.indexWhere((t) => t.id == todo.id); if (index == -1) { // If not exist save the todo. log.info('Create new todo'); todoList.add(todo.copyWith()); } else { // If exist update todo and merge changes only. log.info('Update existing todo'); todoList[index] = todo.copyMerge(todoList[index]); } return todoList; } @override void saveTodo(Todo todo) { log.info('Save todo ${todo.id}'); List todoList = [..._todoList]; updateList(_save(todoList, todo)); } @override void saveMultipleTodos(List todos) { log.info('Save todos ${[for (var t in todos) t.id]}'); List todoList = [..._todoList]; for (var todo in todos) { todoList = _save(todoList, todo); } updateList(todoList); } List _delete(List todoList, Todo todo) { todoList.removeWhere((t) => t.id == todo.id); return todoList; } @override void deleteTodo(Todo todo) { log.info('Delete todo ${todo.id}'); List todoList = [..._todoList]; updateList(_delete(todoList, todo)); } @override void deleteMultipleTodos(List todos) { log.info('Delete todos ${[for (var t in todos) t.id]}'); List todoList = [..._todoList]; for (var todo in todos) { todoList = _delete(todoList, todo); } updateList(todoList); } } class WebDAVTodoListApi extends LocalTodoListApi { final WebDAVFile remoteFile; final WebDAVClient client; WebDAVTodoListApi( super.localFile, this.remoteFile, this.client, ); WebDAVTodoListApi.fromString({ required String localFilePath, required String remoteFilePath, required WebDAVClient client, }) : this( LocalFile(localFilePath), WebDAVFile(remoteFilePath, client), client, ); @override Future initSource() async { await super.initSource(); await client.ping(); if (await client.fileExists(filename: remoteFile.path)) { await readFromSource(); } else { await writeToSource(); } } @override Future readFromSource() async { await write(await downloadFromSource()); await super.readFromSource(); } @override Future writeToSource() async { await super.writeToSource(); await uploadToSource(); } Future downloadFromSource() async { log.info('Download todos from server'); return await client.download(filename: remoteFile.path); } Future uploadToSource() async { log.info('Upload todos to server'); await client.upload( filename: remoteFile.path, content: _todoList.join(Platform.lineTerminator), ); } } ================================================ FILE: lib/todo/model/todo_model.dart ================================================ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:equatable/equatable.dart'; import 'package:ntodotxt/common/exception/exceptions.dart'; enum Priority { none, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z } extension Priorities on Priority { static Set get priorities => { for (var p in Priority.values) if (p != Priority.none) p, }; static Set get priorityNames => { for (var p in Priority.values) if (p != Priority.none) p.name, }; static Priority byName(String name) { try { return Priority.values.byName(name); } on Exception { // Returns Priortity.none } return Priority.none; } static Set sort(Set priorities) { List sorted = priorities.toList() ..sort( (Priority a, Priority b) => a.index.compareTo(b.index), ); return sorted.toSet(); } } /// /// Structure of a valid todo string /// with some modification (https://github.com/todotxt/todo.txt/discussions/52) /// /// [completion] (optional) /// [completionDate] (forbidden if incompleted, mandatory if completed) /// [priority] (optional) /// [creationDate] (optional) /// [ /// /// [fullDescription] (description + tags: projects, context, keyValues /// can be placed anywhere here) /// [description] (mandatory) /// [projects] (optional: preceded by a single space and a '+', /// contains any non-whitespace character) /// [contexts] (optional: preceded by a single space and a '@', /// contains any non-whitespace character) /// [keyValues] (optional: separated by a single colon, /// key value contains any non-whitespace character which are not colons) /// ] /// /// VALID examples: /// /// Write some tests (When incomplete, no date is required) /// 2019-07-01 Write some tests (The provided date is the creation date) /// x 2019-07-03 Write some test (The provided date is the completion date) /// x 2019-07-03 2019-07-01 Write some test (The provided dates are, in order, completion then creation) /// /// INVALID examples: /// /// 2019-07-03 2019-07-01 Write some tests (The task is incomplete, so can't have a completion date) /// x Write some tests (A completed task needs at least a completion date) /// class Todo extends Equatable { static final RegExp patternWord = RegExp(r'^\S+$'); // Limit priorities from A-F. static final RegExp patternPriority = RegExp(r'^\((?[A-Z])\)$'); static final RegExp patternDate = RegExp(r'^\d{4}-\d{2}-\d{2}$'); static final RegExp patternProject = RegExp(r'^\+\S+$'); static final RegExp patternContext = RegExp(r'^\@\S+$'); static final RegExp patternKeyValue = RegExp(r'^([\w\-\.]+):([^\s:]+)$'); final String id; /// Whether the [Todo] is completed. /// Defaults to null (unset). final bool? _completion; /// The priority of the [Todo]. /// Priorities are A, B, C, ... /// Defaults to null (empty, unset). final Priority? _priority; /// The completion date of the [Todo]. /// Defaults to null (unset). final DateTime? _completionDate; /// The creation date of the [Todo]. /// Defaults to null (unser). final DateTime? _creationDate; /// The description of the [Todo]. /// Defaults to null (unset). final String? _description; /// Whether the [Todo] is completed. /// Defaults to false. bool get completion => _completion ?? false; String get fmtCompletion => completion ? 'x' : ''; /// The priority of the [Todo]. /// Allowed values are A, B, C, ... or null (no empty string). /// Empty string value is used to reset the priority. Priority get priority => _priority ?? Priority.none; String get fmtPriority { if (priority == Priority.none) { return ''; } return '(${priority.name})'; } /// The completion date of the [Todo]. /// Defaults to null. DateTime? get completionDate => _completionDate; String get fmtCompletionDate => date2Str(completionDate) ?? ''; /// The creation date of the [Todo]. /// Defaults to null. DateTime? get creationDate => _creationDate; String get fmtCreationDate => date2Str(creationDate) ?? ''; /// The description of the [Todo]. /// Returns the description or an empty string (if null). String get description => _description ?? ''; String get fmtDescription { final List descriptionList = []; for (String item in description.split(' ')) { if (matchProject(item)) continue; if (matchContext(item)) continue; if (matchKeyValue(item)) continue; descriptionList.add(item); } return descriptionList.join(' '); } /// The list of contexts of the [Todo]. /// Defaults to an empty [Set]. Set get projects { List projects = []; for (String item in description.split(' ')) { if (matchProject(item)) { projects.add(item.substring(1)); } } return (projects..sort()).toSet(); } Set get fmtProjects => {for (var p in projects) '+$p'}; static fmtProject(String p) => p.startsWith('+') ? p : '+$p'; bool containsProject(String project) { if (project.startsWith('+')) { return projects.contains(project.substring(1)); } else { return projects.contains(project); } } static bool matchProject(String project) => patternProject.hasMatch(project); /// The list of contexts of the [Todo]. /// Defaults to an empty [Set]. Set get contexts { List contexts = []; for (String item in description.split(' ')) { if (matchContext(item)) { contexts.add(item.substring(1)); } } return (contexts..sort()).toSet(); } Set get fmtContexts => {for (var c in contexts) '@$c'}; static fmtContext(String c) => c.startsWith('@') ? c : '@$c'; bool containsContext(String context) { if (context.startsWith('@')) { return contexts.contains(context.substring(1)); } else { return contexts.contains(context); } } static bool matchContext(String context) => patternContext.hasMatch(context); /// The list of key value pairs of the [Todo]. /// Defaults to an empty [Map]. Set get keyValues { List keyValues = []; for (String item in description.split(' ')) { if (matchKeyValue(item)) { List kvSplitted = item.split(':'); if (kvSplitted.length > 2) continue; keyValues.add(item); } } return (keyValues..sort()).toSet(); } Set get fmtKeyValues => keyValues; static fmtKeyValue(String keyValue) => keyValue; /// Checks if a key value pair with specific key already exists. bool containsKeyValue(String keyValue) { for (String kv in keyValues) { if (kv.split(':')[0] == keyValue.split(':')[0]) { return true; } } return false; } static bool matchKeyValue(String kv) => patternKeyValue.hasMatch(kv); DateTime? get dueDate { for (String kv in keyValues) { List kvSplitted = kv.split(':'); if (kvSplitted[0] == 'due') { return str2date(kvSplitted[1]); } } return null; } String get fmtDueDate { final String? dueDateStr = date2Str(dueDate); return dueDateStr != null ? 'due:$dueDateStr' : ''; } // Core todo constructor with validation logic. Todo._({ required this.id, bool? completion, Priority? priority, DateTime? completionDate, DateTime? creationDate, String? description, }) : _completion = completion, _priority = priority, _completionDate = completionDate, _creationDate = creationDate, _description = description { // Validate completion date. if (completion == true) { if (completionDate == null) { // A completed todo needs at least a completion date. throw const TodoMissingCompletionDate(); } } else { if (completionDate != null) { // A completed todo cannot have a completion date. throw const TodoForbiddenCompletionDate(); } } } /// Factory for model creation with safety mechanisms. factory Todo({ String? id, bool? completion, Priority? priority, DateTime? completionDate, DateTime? creationDate, String? description, }) { final DateTime now = DateTime.now(); if (completion == true) { if (completionDate == null) { completionDate = DateTime(now.year, now.month, now.day); } else { completionDate = DateTime( completionDate.year, completionDate.month, completionDate.day); } } else { completionDate = null; } if (creationDate == null) { // Initialize creationDate to be sure there is always one set. creationDate = DateTime(now.year, now.month, now.day); } else { creationDate = DateTime(creationDate.year, creationDate.month, creationDate.day); } return Todo._( id: id ?? Todo.genId(), completion: completion, priority: priority, completionDate: completionDate, creationDate: creationDate, description: description != null ? description.replaceAllMapped(RegExp(r'\s{2,}'), (match) => ' ') : description, ); } factory Todo.fromString({ String? id, required String value, }) { final todoStr = value.trim().replaceAllMapped(RegExp(r'\s{2,}'), (match) => ' '); bool completion = _str2completion( _todoStringElementAt(todoStr, 0), ); Priority priority; DateTime? completionDate; DateTime? creationDate; List? fullDescriptionList; // Get completion if (completion) { completionDate = str2date( _todoStringElementAt(todoStr, 1), ); priority = _str2priority( _todoStringElementAt(todoStr, 2), ); // x [] [] if (priority == Priority.none) { creationDate = str2date( _todoStringElementAt(todoStr, 2), ); } else { creationDate = str2date( _todoStringElementAt(todoStr, 3), ); } } else { priority = _str2priority( _todoStringElementAt(todoStr, 0), ); // [] [] if (priority == Priority.none) { // The provided date is the creation date (todo incompleted). creationDate = str2date( _todoStringElementAt(todoStr, 0), ); } else { // The provided date is the creation date (todo incompleted). creationDate = str2date( _todoStringElementAt(todoStr, 1), ); } // The todo is not completed so two dates are forbidden. // Everything that comes after the creationDate is interpreted as the description. } // Get beginning of description. int descriptionIndex = 0; for (var prop in [completion, completionDate, priority, creationDate]) { if (prop != null && prop != false && prop != Priority.none) { descriptionIndex += 1; } } try { fullDescriptionList = todoStr.split(' ').sublist(descriptionIndex); } on RangeError { fullDescriptionList = []; } return Todo( id: id ?? Todo.genId(), completion: completion, priority: priority, completionDate: completionDate, creationDate: creationDate, description: fullDescriptionList.join(' '), // Including tags. ); } /// A regular copyWith function. Todo copyWith({ bool? completion, Priority? priority, DateTime? completionDate, DateTime? creationDate, String? description, }) { return Todo( id: id, completion: completion ?? this.completion, priority: priority ?? this.priority, completionDate: completionDate ?? this.completionDate, creationDate: creationDate ?? this.creationDate, description: description ?? this.description, ); } /// Creates a todo object that only sets the values /// that have been explicitly edited. /// The other values remain to null. Todo copyDiff({ bool? completion, Priority? priority, DateTime? completionDate, DateTime? creationDate, String? description, }) { return Todo( id: id, completion: completion, priority: priority, completionDate: completionDate, // Once the creationDate is set, keep it. creationDate: creationDate ?? this.creationDate, description: description, ); } /// Copy only the explicitly set attributes into the new object. /// Use the existing values for the rest. /// If the values of _ are not null, they have been /// explicitly edited. Todo copyMerge(Todo todo) { return Todo( id: id, completion: _completion ?? todo.completion, priority: _priority ?? todo.priority, completionDate: _completionDate ?? todo.completionDate, creationDate: _creationDate ?? todo.creationDate, description: _description ?? todo.description, ); } @override List get props => [ completion, completionDate, priority, creationDate, description, ]; @override String toString() { final List items = [ fmtCompletion, fmtCompletionDate, fmtPriority, fmtCreationDate, description, ]..removeWhere((value) => value.isEmpty); return items.join(' '); } static String genId({int len = 32}) { final Random r = Random(); const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; final String randomId = List.generate(len, (index) => chars[r.nextInt(chars.length)]).join(); return sha256.convert(utf8.encode(randomId)).toString(); } static DateTime? str2date(String value) { if (patternDate.hasMatch(value)) { return DateTime.parse(value); } else { return null; } } static String? date2Str(DateTime? date) { if (date == null) { return null; } return "${date.year.toString()}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}"; } static int compareToToday(DateTime date) { final DateTime now = DateTime.now(); final DateTime today = DateTime(now.year, now.month, now.day); return date.compareTo(today); } static String differenceToToday(DateTime date) { final DateTime now = DateTime.now(); final DateTime today = DateTime(now.year, now.month, now.day); final Duration difference = today.difference(date); final int days = difference.inDays; if (days < 0) { return 'In future'; // In real this should never the case. } else if (days == 0) { return 'Today'; } else if (days == 1) { return 'Yesterday'; } else if (days > 1 && days <= 6) { return '$days days ago'; } else if (days > 6 && days <= 30) { final int weeks = days ~/ 7; return weeks == 1 ? '$weeks week ago' : '$weeks weeks ago'; } else if (days > 30 && days <= 364) { final int months = days ~/ 31; return months == 1 ? '$months month ago' : '$months months ago'; } else { final int years = days ~/ 365; return years == 1 ? '$years year ago' : '$years years ago'; } } static String _todoStringElementAt(String value, int index) { final List todoStrSplitted = value.split(' '); try { return todoStrSplitted[index]; } on RangeError { return ''; } } static bool _str2completion(String value) { // A completed task starts with an lowercase x character. return value == 'x'; } static Priority _str2priority(String value) { RegExpMatch? match = patternPriority.firstMatch(value); if (match != null) { String? priority = match.namedGroup('priority'); if (priority != null) { return Priorities.byName(priority); } } // Priority is optional. return Priority.none; } } ================================================ FILE: lib/todo/page/todo_create_edit_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart' show SnackBarHandler; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/date_picker.dart'; import 'package:ntodotxt/common/widget/key_values_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; class TodoCreateEditPage extends StatelessWidget { final Todo initTodo; final Set projects; final Set contexts; final Set keyValues; final bool newTodo; const TodoCreateEditPage({ required this.initTodo, this.projects = const {}, this.contexts = const {}, this.keyValues = const {}, this.newTodo = true, super.key, }); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => TodoCubit(todo: initTodo), child: GestureDetector( onTap: () { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: TodoDialogWrapper( initTodo: initTodo, newTodo: newTodo, child: Scaffold( appBar: MainAppBar( title: newTodo ? 'Create' : 'Edit', toolbar: Row( children: [ if (!newTodo) const DeleteTodoIconButton(), SaveTodoIconButton(initTodo: initTodo), ], ), ), body: ListView( children: [ const TodoDescriptionTextField(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'General', style: Theme.of(context).textTheme.titleSmall, ), ), ), const TodoPriorityItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Dates', style: Theme.of(context).textTheme.titleSmall, ), ), ), const TodoCreationDateItem(), if (!newTodo) const TodoCompletionDateItem(), const TodoDueDateItem(), const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( title: Text( 'Tags', style: Theme.of(context).textTheme.titleSmall, ), ), ), TodoProjectTagsItem(availableTags: projects), TodoContextTagsItem(availableTags: contexts), TodoKeyValueTagsItem(availableTags: keyValues), const SizedBox(height: 16), ], ), floatingActionButton: !newTodo ? DoneUndonePrimaryButton() : null, ), ), ), ); } } class TodoDialogWrapper extends StatelessWidget { final Widget child; final Todo? initTodo; final bool newTodo; const TodoDialogWrapper({ required this.child, required this.initTodo, required this.newTodo, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoState state) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, T? result) async { if (didPop) { return; } if (state.todo.description.isEmpty) { if (!await ConfirmationDialog.dialog( context: context, title: newTodo ? 'Create todo' : 'Edit todo', message: 'Cannot save a todo with an empty name.', cancelLabel: 'Cancel', actionLabel: 'Continue', )) { if (context.mounted) { context.pop(); } } } else { if (initTodo != state.todo) { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Save todo', message: 'Todo contains unsaved changes. These will be irrecoverably lost.', actionLabel: 'Save', cancelLabel: 'Discard', ); if (context.mounted && confirm) { context .read() .add(TodoListTodoSubmitted(todo: state.todo)); if (newTodo) { if (context.mounted) { SnackBarHandler.info(context, 'Todo has been created'); } } else { if (context.mounted) { SnackBarHandler.info(context, 'Todo has been updated'); } } } } if (context.mounted) { context.pop(); } } }, child: child, ); }, ); } } class DoneUndonePrimaryButton extends StatelessWidget { const DoneUndonePrimaryButton({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoState state) { return FloatingActionButton( tooltip: state.todo.completion ? 'Done' : 'Undone', child: state.todo.completion ? Icon(Icons.remove_done) : Icon(Icons.done_all), onPressed: () async { if (state.todo.completion) { unsetCompletionDate(context); } else { setCompletionDate(context, state); } }, ); }, ); } void setCompletionDate(BuildContext context, TodoState state) { context.read().toggleCompletion( completion: true, completionDate: DateTime.now(), ); } void unsetCompletionDate(BuildContext context) { context.read().toggleCompletion( completion: false, ); } } class SaveTodoIconButton extends StatelessWidget { final Todo? initTodo; const SaveTodoIconButton({ required this.initTodo, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoState state) { return Visibility( visible: initTodo != state.todo && state.todo.description.isNotEmpty, child: IconButton( tooltip: 'Save', icon: const Icon(Icons.save), onPressed: () async { context .read() .add(TodoListTodoSubmitted(todo: state.todo)); if (context.mounted) { SnackBarHandler.info(context, 'Todo saved'); context.pop(); } }, ), ); }, ); } } class DeleteTodoIconButton extends StatelessWidget { const DeleteTodoIconButton({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoState state) { return IconButton( tooltip: 'Delete', icon: const Icon(Icons.delete), onPressed: () async { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Delete todo', message: 'Do you want to delete the todo?', actionLabel: 'Delete', cancelLabel: 'Cancel', ); if (context.mounted && confirm) { context .read() .add(TodoListTodoDeleted(todo: state.todo)); if (context.mounted) { SnackBarHandler.info(context, 'Todo has been deleted'); context.pop(); } } }, ); }, ); } } class TodoDescriptionTextField extends StatefulWidget { const TodoDescriptionTextField({super.key}); @override State createState() => _TodoDescriptionTextFieldState(); } class _TodoDescriptionTextFieldState extends State { late GlobalKey _textFormKey; late TextEditingController _controller; @override void initState() { super.initState(); _textFormKey = GlobalKey(); _controller = TextEditingController(); } @override void dispose() { // Clean up the controller when the widget is disposed. _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoState state) { // Setting text and selection together. int base = _controller.selection.base.offset; _controller.value = _controller.value.copyWith( text: state.todo.description, selection: TextSelection.fromPosition( TextPosition( offset: base < 0 || base > state.todo.description.length ? state.todo.description.length : base, ), ), ); return TextFormField( key: _textFormKey, controller: _controller, minLines: 1, maxLines: 3, keyboardType: TextInputType.text, inputFormatters: [ FilteringTextInputFormatter.deny(RegExp(r'\n')), ], style: Theme.of(context).textTheme.titleMedium, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( hintText: 'todo +project @context key:val', contentPadding: EdgeInsets.symmetric( horizontal: 20.0, vertical: 16.0, ), ), onChanged: (String value) => context.read().updateDescription(_controller.text), ); }, ); } } class TodoPriorityItem extends StatelessWidget { const TodoPriorityItem({ super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.priority != state.todo.priority; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.outlined_flag), title: const Text('Priority'), subtitle: Text(state.todo.priority.name), onTap: () => TodoPriorityTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: Priorities.priorities, ), ), ); }, ); } } class TodoProjectTagsItem extends StatelessWidget { final Set availableTags; const TodoProjectTagsItem({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.projects != state.todo.projects; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.rocket_launch_outlined), title: const Text('Projects'), subtitle: state.todo.projects.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.todo.projects) BasicChip(label: t), ], ), onTap: () => TodoProjectTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: availableTags, ), ), ); }, ); } } class TodoContextTagsItem extends StatelessWidget { final Set availableTags; const TodoContextTagsItem({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.contexts != state.todo.contexts; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.tag), title: const Text('Contexts'), subtitle: state.todo.contexts.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.todo.contexts) BasicChip(label: t), ], ), onTap: () => TodoContextTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: availableTags, ), ), ); }, ); } } class TodoKeyValueTagsItem extends StatelessWidget { final Set availableTags; const TodoKeyValueTagsItem({ this.availableTags = const {}, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.keyValues != state.todo.keyValues; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.join_inner_outlined), title: const Text('Key values'), subtitle: state.todo.fmtKeyValues.isEmpty ? const Text('-') : GenericChipGroup( children: [ for (var t in state.todo.fmtKeyValues) BasicChip(label: t), ], ), onTap: () => TodoKeyValueTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: availableTags, ), ), ); }, ); } } class TodoCreationDateItem extends StatelessWidget { const TodoCreationDateItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.creationDate != state.todo.creationDate; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.edit_calendar), title: const Text('Creation date'), subtitle: Text( state.todo.creationDate != null ? state.todo.fmtCreationDate : Todo.date2Str(DateTime.now())!, ), ), ); }, ); } } class TodoCompletionDateItem extends StatelessWidget { const TodoCompletionDateItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.completionDate != state.todo.completionDate; }, builder: (BuildContext context, TodoState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.event_available), title: const Text('Completion date'), subtitle: Text( state.todo.completionDate != null ? state.todo.fmtCompletionDate : '-', ), onTap: () async => await setCompletionDate(context, state), ), ); }, ); } Future setCompletionDate(BuildContext context, TodoState state) async { final DateTime? date = await TodoDatePicker.pickDate( context: context, initialDate: state.todo.completionDate, endDateDaysOffset: 0, // You can not complete a todo in the future. ); if (date != null) { final String? formattedDate = Todo.date2Str(date); if (formattedDate != null) { if (context.mounted) { context.read().toggleCompletion( completion: true, completionDate: date, ); } } } } } class TodoDueDateItem extends StatelessWidget { const TodoDueDateItem({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoState previousState, TodoState state) { return previousState.todo.keyValues != state.todo.keyValues; }, builder: (BuildContext context, TodoState state) { final String? dueDate = Todo.date2Str(state.todo.dueDate); return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, leading: const Icon(Icons.event), title: const Text('Due date'), subtitle: Text(dueDate ?? '-'), trailing: state.todo.dueDate == null ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () => unsetDueDate(context, dueDate!), ), onTap: () async => await setDueDate(context, state), ), ); }, ); } Future setDueDate(BuildContext context, TodoState state) async { final DateTime? date = await TodoDatePicker.pickDate( context: context, initialDate: state.todo.dueDate, startDateDaysOffset: 0, // You can not set due date in the past. ); if (date != null) { final String? formattedDate = Todo.date2Str(date); if (formattedDate != null) { if (context.mounted) { context.read().addKeyValue('due:$formattedDate'); } } } } void unsetDueDate(BuildContext context, String dueDate) { context.read().removeKeyValue('due:$dueDate'); } } ================================================ FILE: lib/todo/page/todo_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/misc.dart' show PopScopeDrawer, SnackBarHandler; import 'package:ntodotxt/common/widget/app_bar.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; import 'package:ntodotxt/common/widget/scroll_to_top.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/setting/state/interaction_settings_state.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; class TodoListPage extends StatelessWidget { final Filter? filter; const TodoListPage({ this.filter, super.key, }); @override Widget build(BuildContext context) => filter == null ? _build(context) : _buildWithFilter(context); Widget _build(BuildContext context) { // @todo: Activate WideLayout later! return const TodoListViewNarrow(); // final bool isNarrowLayout = // MediaQuery.of(context).size.width < maxScreenWidthCompact; // return isNarrowLayout // ? const TodoListViewNarrow() // : const TodoListViewWide(); } Widget _buildWithFilter(BuildContext context) { return BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: filter, )..load(), child: Builder( builder: (BuildContext context) => _build(context), ), ); } } /// /// Narrow layout /// class TodoListViewNarrow extends ScollToTopView { const TodoListViewNarrow({super.key}); @override State createState() => _TodoListViewNarrowState(); } class _TodoListViewNarrowState extends ScollToTopViewState { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoListState previousState, TodoListState todoListState) => (previousState is! TodoListLoading && todoListState is TodoListLoading) || (previousState is TodoListLoading && todoListState is! TodoListLoading), builder: (BuildContext context, TodoListState todoListState) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState filterState) => previousState.filter.name != filterState.filter.name, builder: (BuildContext context, FilterState filterState) { return PopScopeDrawer( child: Scaffold( appBar: MainAppBar( title: filterState.filter.name.isEmpty ? 'Todos' : 'Filter: ${filterState.filter.name}', toolbar: Row( children: [ const TodoListDeleteFilter(), const TodoListSaveFilter(), IconButton( tooltip: 'Search', icon: const Icon(Icons.search), onPressed: () => context.pushNamed('todo-search', extra: filterState.filter), ), ], ), bottom: const AppBarFilterList(), ), drawer: Container(), floatingActionButton: scrolledDown ? FloatingActionButton.small( tooltip: 'Go to top', foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer, child: const Icon(Icons.keyboard_arrow_up), onPressed: () => scrollToTop(), ) : FloatingActionButton( tooltip: 'Add todo', child: const Icon(Icons.add), onPressed: () { String initDescription = ''; if (filterState.filter.projects.isNotEmpty) { initDescription = '+${filterState.filter.projects.join(' +')}'; } if (filterState.filter.contexts.isNotEmpty) { initDescription = '$initDescription @${filterState.filter.contexts.join(' @')}'; } context.pushNamed( 'todo-create', extra: Todo.fromString(value: initDescription), ); }, ), body: RefreshIndicator( onRefresh: () async { context .read() .add(const TodoListSynchronizationRequested()); }, child: LoadingIndicatorWrapper( loading: todoListState is TodoListLoading, child: TodoList(scrollController: scrollController), ), ), ), ); }, ); }, ); } } /// /// Wide layout /// class TodoListViewWide extends ScollToTopView { const TodoListViewWide({super.key}); @override State createState() => _TodoListViewWideState(); } class _TodoListViewWideState extends ScollToTopViewState { @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (TodoListState previousState, TodoListState todoListState) => (previousState is! TodoListLoading && todoListState is TodoListLoading) || (previousState is TodoListLoading && todoListState is! TodoListLoading), builder: (BuildContext context, TodoListState todoListState) { return BlocBuilder( buildWhen: (FilterState previousState, FilterState filterState) => previousState.filter.name != filterState.filter.name, builder: (BuildContext context, FilterState filterState) { return PopScopeDrawer( child: Scaffold( appBar: MainAppBar( title: filterState.filter.name.isEmpty ? 'Todos' : 'Filter: ${filterState.filter.name}', toolbar: Row( children: [ const TodoListDeleteFilter(), const TodoListSaveFilter(), IconButton( tooltip: 'Search', icon: const Icon(Icons.search), onPressed: () => context.pushNamed('todo-search', extra: filterState.filter), ), ], ), bottom: const AppBarFilterList(), ), floatingActionButton: scrolledDown ? FloatingActionButton.small( tooltip: 'Go to top', foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer, backgroundColor: Theme.of(context).colorScheme.secondaryContainer, child: const Icon(Icons.keyboard_arrow_up), onPressed: () => scrollToTop(), ) : FloatingActionButton( tooltip: 'Add todo', child: const Icon(Icons.add), onPressed: () { String initDescription = ''; if (filterState.filter.projects.isNotEmpty) { initDescription = '+${filterState.filter.projects.join(' +')}'; } if (filterState.filter.contexts.isNotEmpty) { initDescription = '$initDescription @${filterState.filter.contexts.join(' @')}'; } context.pushNamed( 'todo-create', extra: Todo.fromString(value: initDescription), ); }, ), body: RefreshIndicator( onRefresh: () async { context .read() .add(const TodoListSynchronizationRequested()); }, child: LoadingIndicatorWrapper( loading: todoListState is TodoListLoading, child: TodoList(scrollController: scrollController), ), ), ), ); }, ); }, ); } } /// /// Components /// class LoadingIndicatorWrapper extends StatelessWidget { final Widget child; final bool loading; const LoadingIndicatorWrapper({ required this.child, this.loading = false, super.key, }); @override Widget build(BuildContext context) { if (loading) { return Stack( children: [ child, // Custom progress indicator. Align( alignment: Alignment.topCenter, child: Container( padding: const EdgeInsets.only(top: 50), child: Container( height: 40, width: 40, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(100), ), child: const Padding( padding: EdgeInsets.all(10), child: CircularProgressIndicator(strokeWidth: 3), ), ), ), ), ], ); } else { return child; } } } class TodoList extends StatelessWidget { final ScrollController scrollController; const TodoList({ required this.scrollController, super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, TodoListState todoListState) { return BlocBuilder( builder: (BuildContext context, FilterState filterState) { final Map?> sectionList = todoListState.groupedTodoList( filterState.filter, ); return ListView.builder( key: const PageStorageKey('TodoList'), controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), itemCount: sectionList.length, itemBuilder: (BuildContext context, int index) { String section = sectionList.keys.elementAt(index); Iterable todoList = sectionList[section]!; return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: PageStorageKey(section), title: Text( section, style: Theme.of(context).textTheme.titleSmall, ), ), ), for (var todo in todoList) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: TodoListTile(todo: todo), ), if (index < sectionList.length - 1) const Divider(), ], ); }, ); }, ); }, ); } } class TodoListTile extends StatelessWidget { final Todo todo; TodoListTile({ required this.todo, Key? key, }) : super(key: PageStorageKey(todo.id)); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (InteractionSettingsState previousState, InteractionSettingsState state) => previousState.swipeLeftActionEnabled != state.swipeLeftActionEnabled || previousState.swipeRightActionEnabled != state.swipeRightActionEnabled, builder: (BuildContext context, InteractionSettingsState state) { return Dismissible( key: Key(todo.id), direction: state.swipeLeftActionEnabled && state.swipeRightActionEnabled ? DismissDirection.horizontal : state.swipeLeftActionEnabled ? DismissDirection.endToStart : state.swipeRightActionEnabled ? DismissDirection.startToEnd : DismissDirection.none, dismissThresholds: const { DismissDirection.startToEnd: 0.5, DismissDirection.endToStart: 0.5, }, background: Container( color: Theme.of(context).colorScheme.error, // red alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 16.0), child: Icon(Icons.delete), ), secondaryBackground: Container( color: Theme.of(context).colorScheme.primaryContainer, // blue alignment: Alignment.centerRight, padding: const EdgeInsets.only(right: 16.0), child: Icon(todo.completion ? Icons.remove_done : Icons.done_all), ), confirmDismiss: (DismissDirection direction) async { // Delete if (direction == DismissDirection.startToEnd) { return await ConfirmationDialog.dialog( context: context, title: 'Delete todo', message: 'Do you want to delete the todo?', actionLabel: 'Delete', cancelLabel: 'Cancel', ); } return true; }, onDismissed: (DismissDirection direction) async { // Done / Undone if (todo.completion) { SnackBarHandler.info(context, 'Todo has marked as not completed'); } else { SnackBarHandler.info(context, 'Todo has marked as completed'); } if (direction == DismissDirection.endToStart) { context.read().add( TodoListTodoCompletionToggled( todo: todo, completion: !todo.completion, ), ); } else if (direction == DismissDirection.startToEnd) { // Delete context.read().add( TodoListTodoDeleted( todo: todo, ), ); SnackBarHandler.info(context, 'Todo has been deleted'); } }, child: ListTile( key: key, title: _buildTitle(context), subtitle: Padding( padding: const EdgeInsets.only(top: 2.0), child: _buildSubtitle(), ), onTap: () => context.pushNamed('todo-edit', extra: todo), ), ); }, ); } Widget _buildTitle(BuildContext context) { final List items = todo.description.split(' ') ..removeWhere( (String item) => item.startsWith('due:'), ); return RichText( text: TextSpan( style: todo.completion ? Theme.of(context).textTheme.titleMedium?.copyWith( decoration: TextDecoration.lineThrough, decorationThickness: 4.0, ) : Theme.of(context).textTheme.titleMedium, text: '', children: [ for (int i = 0; i < items.length; i++) TextSpan( text: i == items.length - 1 ? items[i] : '${items[i]} ', style: Todo.matchProject(items[i]) || Todo.matchContext(items[i]) || Todo.matchKeyValue(items[i]) ? const TextStyle(fontWeight: FontWeight.bold) : null), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } Widget? _buildSubtitle() { return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 2.0, // gap between adjacent chips runSpacing: 2.0, // gap between lines children: [ if (todo.priority != Priority.none) BasicIconChip( mono: true, iconData: Icons.flag_outlined, label: todo.priority.name, ), if (todo.creationDate != null) BasicIconChip( mono: true, iconData: Icons.edit_calendar, label: Todo.differenceToToday(todo.creationDate!), ), if (todo.completionDate != null && todo.completion) BasicIconChip( mono: true, iconData: Icons.event_available, label: Todo.differenceToToday(todo.completionDate!), ), if (todo.dueDate != null) BasicIconChip( mono: true, iconData: Icons.event, label: Todo.date2Str(todo.dueDate!)!, ) ], ); } } class TodoListDeleteFilter extends StatelessWidget { const TodoListDeleteFilter({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return state.changed || state.filter.id == null ? Container() : IconButton( tooltip: 'Delete filter', icon: const Icon(Icons.delete), onPressed: () async { final bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Delete filter', message: 'Do you want to delete the filter?', actionLabel: 'Delete', cancelLabel: 'Cancel', ); if (context.mounted && confirm) { await context.read().delete(state.filter); if (context.mounted) { SnackBarHandler.info(context, 'Filter deleted'); context.pop(); context.read().back(); } } }, ); }, ); } } class TodoListSaveFilter extends StatelessWidget { const TodoListSaveFilter({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return !state.changed || state.filter.id == null ? Container() : IconButton( tooltip: 'Save filter', icon: const Icon(Icons.save), onPressed: () async { await context .read() .update(state.filter.copyWith()); if (context.mounted) { SnackBarHandler.info(context, 'Filter saved'); } }, ); }, ); } } ================================================ FILE: lib/todo/page/todo_search_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; class TodoSearchPage extends StatelessWidget { final Filter? filter; const TodoSearchPage({ this.filter, super.key, }); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: filter, )..load(), child: Builder( builder: (BuildContext context) => const TodoSearchView(), ), ); } } class TodoSearchView extends StatefulWidget { const TodoSearchView({super.key}); @override State createState() => _TodoSearchViewState(); } class _TodoSearchViewState extends State { String query = ''; late TextEditingController _controller; @override void initState() { super.initState(); _controller = TextEditingController(); _controller.text = query; } @override void dispose() { _controller.dispose(); super.dispose(); } Iterable _getResults(Iterable todoList) { if (query.trim().isEmpty) { return Iterable.empty(); } else { return todoList.where( (Todo t) => t.toString().toLowerCase().contains(query.toLowerCase()), ); } } Widget _buildSearchField(BuildContext context) { return TextFormField( controller: _controller, keyboardType: TextInputType.text, inputFormatters: [ FilteringTextInputFormatter.deny(RegExp(r'\n')), ], style: Theme.of(context).textTheme.titleLarge, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration(hintText: 'Search ...'), onChanged: (String value) => setState(() => query = _controller.text), ); } List _buildActions(BuildContext context) { return [ IconButton( icon: const Icon(Icons.clear), onPressed: query.isEmpty ? null : () { setState(() => query = ''); _controller.text = query; }, ), const SizedBox(width: 8), ]; } @override Widget build(BuildContext context) { // @todo: Activate WideLayout later! // final bool narrowView = // MediaQuery.of(context).size.width < maxScreenWidthCompact; return GestureDetector( onTap: () { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } }, child: Scaffold( appBar: AppBar( // titleSpacing: narrowView ? 0.0 : null, titleSpacing: 0.0, title: _buildSearchField(context), actions: _buildActions(context), ), body: BlocBuilder( builder: (BuildContext context, TodoListState todoListState) { return BlocBuilder( builder: (BuildContext context, FilterState filterState) { final List matchQuery = _getResults( todoListState.filteredTodoList(filterState.filter), ).toList(); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8.0), itemCount: matchQuery.length, itemBuilder: (BuildContext context, int index) { Todo todo = matchQuery[index]; return TodoSearchTile(todo: todo); }, ); }, ); }, ), ), ); } } class TodoSearchTile extends StatelessWidget { final Todo todo; TodoSearchTile({ required this.todo, Key? key, }) : super(key: PageStorageKey(todo.id)); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ListTile( key: key, title: _buildTitle(context), subtitle: Padding( padding: const EdgeInsets.only(top: 2.0), child: _buildSubtitle(), ), onTap: () => _onTapAction(context), ), ); } Widget _buildTitle(BuildContext context) { final List items = todo.description.split(' ') ..removeWhere( (String item) => item.startsWith('due:'), ); return RichText( text: TextSpan( style: todo.completion ? Theme.of(context).textTheme.titleMedium?.copyWith( decoration: TextDecoration.lineThrough, decorationThickness: 4.0, ) : Theme.of(context).textTheme.titleMedium, text: '', children: [ for (int i = 0; i < items.length; i++) TextSpan( text: i == items.length - 1 ? items[i] : '${items[i]} ', style: Todo.matchProject(items[i]) || Todo.matchContext(items[i]) || Todo.matchKeyValue(items[i]) ? const TextStyle(fontWeight: FontWeight.bold) : null), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } Widget? _buildSubtitle() { return Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 2.0, // gap between adjacent chips runSpacing: 2.0, // gap between lines children: [ if (todo.priority != Priority.none) BasicIconChip( mono: true, iconData: Icons.flag_outlined, label: todo.priority.name, ), if (todo.creationDate != null) BasicIconChip( mono: true, iconData: Icons.edit_calendar, label: Todo.differenceToToday(todo.creationDate!), ), if (todo.completionDate != null && todo.completion) BasicIconChip( mono: true, iconData: Icons.event_available, label: Todo.differenceToToday(todo.completionDate!), ), if (todo.dueDate != null) BasicIconChip( mono: true, iconData: Icons.event, label: Todo.date2Str(todo.dueDate!)!, ) ], ); } void _onTapAction(BuildContext context) { context.pushNamed('todo-edit', extra: todo); } } ================================================ FILE: lib/todo/repository/todo_list_repository.dart ================================================ import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; /// A repository that handles `todo` related requests. class TodoListRepository { final TodoListApi api; TodoListRepository(this.api); /// Provides a [Stream] of all todos read from the source. Stream> getTodoList() => api.getTodoList(); Future initSource() => api.initSource(); /// Read [todoList] from source. Future readFromSource() => api.readFromSource(); /// Write [todoList] to source. Future writeToSource() => api.writeToSource(); bool existsTodo(Todo todo) => api.existsTodo(todo); /// Saves a [todo]. /// If a [todo] with [id] already exists, it will be replaced. /// If the [todo] with [id] already exists it will be updated/merged. void saveTodo(Todo todo) => api.saveTodo(todo); /// Saves multiple [todos] by [id] at once. void saveMultipleTodos(List todos) => api.saveMultipleTodos(todos); /// Deletes the given [todo] by [id]. void deleteTodo(Todo todo) => api.deleteTodo(todo); /// Deletes multiple [todos] by [id] at once. void deleteMultipleTodos(List todos) => api.deleteMultipleTodos(todos); } ================================================ FILE: lib/todo/state/todo_cubit.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/exception/exceptions.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; class TodoCubit extends Cubit { TodoCubit({ required Todo todo, }) : super(TodoSuccess(todo: todo)); void updateTodo(Todo todo) { try { emit(state.success( todo: todo.copyWith(), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void toggleCompletion({bool? completion, DateTime? completionDate}) { try { emit(state.success( todo: state.todo.copyWith( completion: completionDate != null ? true : !state.todo.completion, completionDate: completionDate, ), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateDescription(String description) { try { emit(state.success( todo: state.todo.copyWith(description: description), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void setPriority(Priority priority) { try { emit(state.success( todo: state.todo.copyWith(priority: priority), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void unsetPriority() { try { emit(state.success( todo: state.todo.copyWith(priority: Priority.none), )); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addProject(String project) { try { if (state.todo.containsProject(project)) { emit( state.success(todo: state.todo.copyWith()), ); } else { if (!Todo.matchProject(Todo.fmtProject(project))) { throw TodoInvalidProjectTag(tag: project); } emit( state.success( todo: state.todo.copyWith( description: '${state.todo.description} ${Todo.fmtProject(project)}', ), ), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateProjects(Set projects) { try { for (String project in projects) { if (!Todo.matchProject(Todo.fmtProject(project))) { throw TodoInvalidProjectTag(tag: project); } } String description = state.todo.description; Iterable addProjects = projects.where((p) => !state.todo.containsProject(p)); Iterable removeProjects = state.todo.projects.where((p) => !projects.contains(p)); // Remove projects for (String p in removeProjects) { description = description.replaceAll(Todo.fmtProject(p), ''); } // Add projects description = '$description ${{ for (String p in addProjects) Todo.fmtProject(p) }.join(" ")}'; emit( state.success( todo: state.todo.copyWith( description: description, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removeProject(String project) { try { if (!Todo.matchProject(Todo.fmtProject(project))) { throw TodoInvalidProjectTag(tag: project); } emit( state.success( todo: state.todo.copyWith( description: state.todo.description .replaceAll(Todo.fmtProject(project), '') .trim(), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addContext(String context) { try { if (state.todo.containsContext(context)) { emit( state.success(todo: state.todo.copyWith()), ); } else { if (!Todo.matchContext(Todo.fmtContext(context))) { throw TodoInvalidContextTag(tag: context); } emit( state.success( todo: state.todo.copyWith( description: '${state.todo.description} ${Todo.fmtContext(context)}', ), ), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateContexts(Set contexts) { try { for (String context in contexts) { if (!Todo.matchContext(Todo.fmtContext(context))) { throw TodoInvalidContextTag(tag: context); } } String description = state.todo.description; Iterable addContexts = contexts.where((c) => !state.todo.containsContext(c)); Iterable removeContexts = state.todo.contexts.where((c) => !contexts.contains(c)); // Remove projects for (String c in removeContexts) { description = description.replaceAll(Todo.fmtContext(c), ''); } // Add projects description = '$description ${{ for (String c in addContexts) Todo.fmtContext(c) }.join(" ")}'; emit( state.success( todo: state.todo.copyWith( description: description, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removeContext(String context) { try { if (!Todo.matchContext(Todo.fmtContext(context))) { throw TodoInvalidContextTag(tag: context); } emit( state.success( todo: state.todo.copyWith( description: state.todo.description .replaceAll(Todo.fmtContext(context), '') .trim(), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void addKeyValue(String kv) { try { if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) { throw TodoInvalidKeyValueTag(tag: kv); } if (state.todo.containsKeyValue(kv)) { emit( state.success( todo: state.todo.copyWith( description: state.todo.description.replaceAllMapped( RegExp('${kv.split(':')[0]}:\\S+'), (match) => Todo.fmtKeyValue(kv), ), ), ), ); } else { emit( state.success( todo: state.todo.copyWith( description: '${state.todo.description} ${Todo.fmtKeyValue(kv)}', ), ), ); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } void updateKeyValues(Set keyValues) { try { for (String kv in keyValues) { if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) { throw TodoInvalidKeyValueTag(tag: kv); } } String description = state.todo.description; Iterable addKeyValues = keyValues.where((kv) => !state.todo.containsKeyValue(kv)); Iterable removeKeyValues = state.todo.keyValues.where((kv) { for (String keyVal in keyValues) { if (kv.split(':')[0] == keyVal.split(':')[0]) { return false; } } return true; }); Iterable existingKeyValues = keyValues.where((kv) => state.todo.containsKeyValue(kv)); // Remove projects for (String kv in removeKeyValues) { description = description.replaceAll(Todo.fmtKeyValue(kv), ''); } description = '$description ${{ for (String kv in addKeyValues) Todo.fmtKeyValue(kv) }.join(" ")}'; // Replace existing key values instead concat them. for (String kv in existingKeyValues) { description = description.replaceAllMapped( RegExp('${kv.split(':')[0]}:\\S+'), (match) => Todo.fmtKeyValue(kv), ); } emit( state.success( todo: state.todo.copyWith( description: description, ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void removeKeyValue(String kv) { try { if (!Todo.matchKeyValue(Todo.fmtKeyValue(kv))) { throw TodoInvalidKeyValueTag(tag: kv); } emit( state.success( todo: state.todo.copyWith( description: state.todo.description .replaceAll(Todo.fmtKeyValue(kv), '') .trim(), ), ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/todo/state/todo_list_bloc.dart ================================================ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/repository/todo_list_repository.dart' show TodoListRepository; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; class TodoListBloc extends Bloc { final TodoListRepository _repository; TodoListBloc({ required TodoListRepository repository, }) : _repository = repository, super(const TodoListLoading()) { on(_onTodoListSubscriptionRequested); on(_onTodoListSynchronizationRequested); on(_onTodoSubmitted); on(_onTodoDeleted); on(_onTodoCompletionToggled); } Future _onTodoListSubscriptionRequested( TodoListSubscriptionRequested event, Emitter emit, ) async { await emit.forEach>( _repository.getTodoList(), onData: (todoList) { // Use copyWith here to keep the state (e.g. if loading) return state.copyWith(todoList: todoList); }, onError: (e, _) => state.error(message: e.toString()), ); } void _onTodoListSynchronizationRequested( TodoListSynchronizationRequested event, Emitter emit, ) async { try { // Initialize only if this is the first time. if (state is TodoListLoading) { await _repository .initSource() .whenComplete(() => emit(state.success())); } else { emit(state.loading()); await _repository .readFromSource() .whenComplete(() => emit(state.success())); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } void _onTodoSubmitted( TodoListTodoSubmitted event, Emitter emit, ) async { emit(state.loading()); try { _repository.saveTodo(event.todo.copyWith()); await _repository .writeToSource() .whenComplete(() => emit(state.success())); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void _onTodoDeleted( TodoListTodoDeleted event, Emitter emit, ) async { emit(state.loading()); try { _repository.deleteTodo(event.todo.copyWith()); await _repository .writeToSource() .whenComplete(() => emit(state.success())); } on Exception catch (e) { emit(state.error(message: e.toString())); } } void _onTodoCompletionToggled( TodoListTodoCompletionToggled event, Emitter emit, ) async { emit(state.loading()); try { if (_repository.existsTodo(event.todo)) { _repository.saveTodo( event.todo.copyDiff(completion: event.completion), ); } else { _repository.saveTodo( event.todo.copyWith(completion: event.completion), ); } await _repository .writeToSource() .whenComplete(() => emit(state.success())); } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/todo/state/todo_list_event.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; sealed class TodoListEvent extends Equatable { const TodoListEvent(); @override List get props => []; } final class TodoListSubscriptionRequested extends TodoListEvent { const TodoListSubscriptionRequested(); } final class TodoListSynchronizationRequested extends TodoListEvent { const TodoListSynchronizationRequested(); } final class TodoListTodoSubmitted extends TodoListEvent { final Todo todo; const TodoListTodoSubmitted({ required this.todo, }); @override List get props => [todo]; } final class TodoListTodoDeleted extends TodoListEvent { final Todo todo; const TodoListTodoDeleted({ required this.todo, }); @override List get props => [todo]; } final class TodoListTodoCompletionToggled extends TodoListEvent { final Todo todo; final bool completion; const TodoListTodoCompletionToggled({ required this.todo, required this.completion, }); @override List get props => [todo, completion]; } ================================================ FILE: lib/todo/state/todo_list_state.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; sealed class TodoListState extends Equatable { final List todoList; const TodoListState({ this.todoList = const [], }); /// Returns a list with all projects of all todos. Set get projects { // @todo: Sort return todoList.map((Todo todo) => todo.projects).fold>( {}, (Set previousValue, Set value) => previousValue..addAll(value), ); } /// Returns a list with all contexts of all todos. Set get contexts { // @todo: Sort return todoList.map((Todo todo) => todo.contexts).fold>( {}, (Set previousValue, Set value) => previousValue..addAll(value), ); } /// Returns a list with all key values of all todos. Set get keyValues { // @todo: Sort return todoList.map((Todo todo) => todo.fmtKeyValues).fold>( {}, (Set previousValue, Set value) => previousValue..addAll(value), ); } Iterable filteredTodoList(Filter filter) => filter.apply(todoList); Map> groupedTodoList(Filter filter) => filter.grouped(filteredTodoList(filter)); TodoListState copyWith({ List? todoList, }); TodoListState loading({ List? todoList, }) { return TodoListLoading( todoList: todoList ?? this.todoList, ); } TodoListState success({ List? todoList, }) { return TodoListSuccess( todoList: todoList ?? this.todoList, ); } TodoListState error({ required String message, List? todoList, }) { return TodoListError( message: message, todoList: todoList ?? this.todoList, ); } @override List get props => [ todoList, ]; @override String toString() => 'TodoListState { }'; } final class TodoListLoading extends TodoListState { const TodoListLoading({ super.todoList, }); @override TodoListLoading copyWith({ List? todoList, }) { return TodoListLoading( todoList: todoList ?? this.todoList, ); } @override String toString() => 'TodoListLoading { todos: ${[for (var t in todoList) t]} }'; } final class TodoListSuccess extends TodoListState { const TodoListSuccess({ super.todoList, }); @override TodoListSuccess copyWith({ List? todoList, }) { return TodoListSuccess( todoList: todoList ?? this.todoList, ); } @override String toString() => 'TodoListSuccess { todos: ${[for (var t in todoList) t]} }'; } final class TodoListError extends TodoListState { final String message; const TodoListError({ required this.message, super.todoList, }); @override TodoListError copyWith({ String? message, List? todoList, }) { return TodoListError( message: message ?? this.message, todoList: todoList ?? this.todoList, ); } @override List get props => [ message, todoList, ]; @override String toString() => 'TodoListError { message: $message }'; } ================================================ FILE: lib/todo/state/todo_state.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; sealed class TodoState extends Equatable { final Todo todo; const TodoState({ required this.todo, }); TodoSuccess success({ Todo? todo, }) { return TodoSuccess( todo: todo ?? this.todo, ); } TodoError error({ required String message, Todo? todo, }) { return TodoError( message: message, todo: todo ?? this.todo, ); } @override List get props => [ todo, ]; @override String toString() => 'TodoState { todo: $todo }'; } final class TodoSuccess extends TodoState { const TodoSuccess({ required super.todo, }); TodoSuccess copyWith({ Todo? todo, }) { return TodoSuccess( todo: todo ?? this.todo, ); } @override List get props => [ todo, ]; @override String toString() => 'TodoSuccess { todo: "$todo" }'; } final class TodoError extends TodoState { final String message; const TodoError({ required this.message, required super.todo, }); TodoError copyWith({ String? message, Todo? todo, }) { return TodoError( message: message ?? this.message, todo: todo ?? this.todo, ); } @override List get props => [ message, todo, ]; @override String toString() => 'TodoError { message: $message todo: "$todo" }'; } ================================================ FILE: lib/todo_file/state/todo_file_cubit.dart ================================================ import 'dart:io'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ntodotxt/common/constants/app.dart'; import 'package:ntodotxt/main.dart' show log; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; class TodoFileCubit extends Cubit { final SettingRepository repository; TodoFileCubit({ required this.repository, String todoFilename = defaultTodoFilename, String doneFilename = defaultDoneFilename, String localPath = defaultLocalTodoPath, String remotePath = defaultRemoteTodoPath, TodoFileState? state, }) : super( state ?? TodoFileLoading( todoFilename: todoFilename, doneFilename: doneFilename, localPath: localPath, remotePath: remotePath, ), ); Future checkLocalPermission(String filename) async { try { await File(filename).create(); } catch (e) { rethrow; } } Future load() async { try { log.info('Retrieving setting \'todoFilename\'.'); Setting? todoFilename = await repository.get(key: 'todoFilename'); if (todoFilename != null) { emit(state.load(todoFilename: todoFilename.value)); } log.info('Retrieving setting \'localPath\'.'); final Setting? localPath = await repository.get(key: 'localPath'); if (localPath != null) { emit(state.load(localPath: localPath.value)); } await checkLocalPermission(state.localTodoFilePath); log.info('Retrieving setting \'remotePath\'.'); final Setting? remotePath = await repository.get(key: 'remotePath'); if (remotePath != null) { emit(state.load(remotePath: remotePath.value)); } emit(state.ready()); } on Exception catch (e) { emit( state.error( message: e.toString(), todoFilename: defaultTodoFilename, doneFilename: defaultDoneFilename, localPath: defaultLocalTodoPath, remotePath: defaultRemoteTodoPath, ), ); } } Future saveLocalPath(String? value) async { try { if (value != null) { emit(state.load()); log.fine('Saving setting \'localPath\'.'); await repository.updateOrInsert( Setting(key: 'localPath', value: value), ); emit(state.ready(localPath: value)); } } on Exception catch (e) { emit( state.error( message: e.toString(), localPath: value, ), ); } } Future saveLocalFilename(String? value) async { try { if (value != null) { emit(state.load()); log.fine('Saving setting \'todoFilename\'.'); await repository.updateOrInsert( Setting(key: 'todoFilename', value: value), ); emit(state.ready(todoFilename: value)); } } on Exception catch (e) { emit( state.error( message: e.toString(), todoFilename: value, ), ); } } Future saveRemotePath(String? value) async { try { if (value != null) { emit(state.load()); log.fine('Saving setting \'remotePath\'.'); await repository.updateOrInsert( Setting(key: 'remotePath', value: value), ); emit(state.ready(remotePath: value)); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future resetToDefaults() async { try { emit(state.load()); log.fine('Resetting to the defaults.'); await resetTodoFileSettings(); emit( TodoFileLoading( todoFilename: defaultTodoFilename, doneFilename: defaultDoneFilename, localPath: defaultLocalTodoPath, remotePath: defaultRemoteTodoPath, ), ); } on Exception catch (e) { emit(state.error(message: e.toString())); } } Future resetTodoFileSettings() async { try { log.fine('Resetting todofile settings.'); for (var k in [ 'todoFilename', 'localPath', 'remotePath', ]) { log.fine('Deleting setting \'$k\'.'); await repository.delete(key: k); } } on Exception catch (e) { emit(state.error(message: e.toString())); } } } ================================================ FILE: lib/todo_file/state/todo_file_state.dart ================================================ import 'dart:io'; import 'package:equatable/equatable.dart'; import 'package:ntodotxt/common/constants/app.dart'; sealed class TodoFileState extends Equatable { final String todoFilename; final String doneFilename; final String localPath; final String remotePath; TodoFileState({ this.todoFilename = defaultTodoFilename, this.doneFilename = defaultDoneFilename, String localPath = defaultLocalTodoPath, String remotePath = defaultLocalTodoPath, }) : localPath = localPath.endsWith(Platform.pathSeparator) ? localPath : '$localPath${Platform.pathSeparator}', remotePath = remotePath.endsWith(Platform.pathSeparator) ? remotePath : '$remotePath${Platform.pathSeparator}'; String get localTodoFilePath => '$localPath$todoFilename'; String get remoteTodoFilePath => '$remotePath$todoFilename'; TodoFileLoading load({ String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileLoading( todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } TodoFileReady ready({ String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileReady( todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } TodoFileError error({ required String message, String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileError( message: message, todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } @override List get props => [ todoFilename, doneFilename, localPath, remotePath, ]; @override String toString() => 'TodoFileState { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }'; } final class TodoFileLoading extends TodoFileState { TodoFileLoading({ super.todoFilename, super.doneFilename, super.localPath, super.remotePath, }); TodoFileLoading copyWith({ String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileLoading( todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } @override String toString() => 'TodoFileLoading { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }'; } final class TodoFileReady extends TodoFileState { TodoFileReady({ super.todoFilename, super.doneFilename, super.localPath, super.remotePath, }); TodoFileReady copyWith({ String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileReady( todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } @override String toString() => 'TodoFileReady { localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }'; } final class TodoFileError extends TodoFileState { final String message; TodoFileError({ required this.message, super.todoFilename, super.doneFilename, super.localPath, super.remotePath, }); TodoFileError copyWith({ String? message, String? todoFilename, String? doneFilename, String? localPath, String? remotePath, }) { return TodoFileError( message: message ?? this.message, todoFilename: todoFilename ?? this.todoFilename, doneFilename: doneFilename ?? this.doneFilename, localPath: localPath ?? this.localPath, remotePath: remotePath ?? this.remotePath, ); } @override List get props => [ message, todoFilename, doneFilename, localPath, remotePath, ]; @override String toString() => 'TodoFileError { message $message localFile $localTodoFilePath remoteFile: $remoteTodoFilePath }'; } ================================================ FILE: lib/webdav/client/webdav_client.dart ================================================ import 'dart:convert' show utf8; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:ntodotxt/main.dart' show log; import 'package:path/path.dart' as p; import 'package:webdav_client/webdav_client.dart' as webdav; class WebDAVClientException implements Exception { final String message; const WebDAVClientException(this.message); @override String toString() => message; } class WebDAVClient { webdav.Client? _client; // Singleton pattern late final String scheme; late final String host; late final int? port; late final String path; final String username; final String password; final bool acceptUntrustedCert; static const int idleTimeout = 15000; static const int connectionTimeout = 15000; static const int sendTimeout = 15000; static const int receiveTimeout = 15000; WebDAVClient({ required String server, required String path, required this.username, required this.password, this.acceptUntrustedCert = false, }) { final RegExp exp = RegExp( r'(?^(http|https)):\/\/(?[a-zA-Z0-9.-]+)(:(?\d+)){0,1}$'); final RegExpMatch? match = exp.firstMatch(server); if (match != null) { scheme = match.namedGroup('scheme')!; host = match.namedGroup('host')!; port = match.namedGroup('port') != null ? int.parse(match.namedGroup('port')!) : null; } else { throw const FormatException('Invalid server format'); } if (path == '/') { this.path = path; } else { if (path.endsWith('/')) { this.path = path.substring(0, path.length - 1); } else { this.path = path; } } } Future get connection async { if (_client != null) { return _client!; } else { _client = await _open(); return _client!; } } Future _open() async { // Handle untrusted certificates. webdav.WdDio dio = webdav.WdDio(debug: false); dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { // Don't trust any certificate just because their root cert is trusted. final HttpClient httpClient = HttpClient(context: SecurityContext(withTrustedRoots: false)); httpClient.idleTimeout = const Duration( milliseconds: idleTimeout, ); httpClient.connectionTimeout = const Duration( milliseconds: connectionTimeout, ); // You can test the intermediate / root cert here. We just ignore it. httpClient.badCertificateCallback = (cert, host, port) => acceptUntrustedCert; return httpClient; }, validateCertificate: (cert, host, port) => acceptUntrustedCert, ); webdav.Client client = webdav.Client( uri: Uri( scheme: scheme, host: host, port: port, path: path, ).toString(), c: dio, auth: webdav.BasicAuth(user: username, pwd: password), debug: false, ); client.setHeaders({'accept-charset': 'utf-8'}); client.setConnectTimeout(connectionTimeout); client.setSendTimeout(sendTimeout); client.setReceiveTimeout(receiveTimeout); return client; } (String, String) _handleDioError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return ( 'TIMEOUT', 'Timeout occurred while sending or receiving', ); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode; if (statusCode != null) { switch (statusCode) { case 400: return ( 'BAD REQUEST', 'Something went wrong', ); case 401: return ( 'UNAUTHORIZED', 'It seems that the credentials are incorrect', ); case 403: return ( 'FORBIDDEN', 'The request was rejected by the server', ); case 404: return ( 'NOT FOUND', 'The requested resource could not be found', ); case 405: return ( 'METHOD NOT ALLOWED', 'The request method is not supported for the requested resource', ); case 409: return ( 'CONFLICT', 'The request could not be processed because of conflict in the current state of the resource', ); case 500: return ( 'INTERNAL SERVER ERROR', 'An unexpected error was encountered on server side', ); default: return ( 'INTERNAL SERVER ERROR ($statusCode)', 'An unexpected error was encountered on server side', ); } } break; case DioExceptionType.cancel: break; case DioExceptionType.badCertificate: return ( 'INVALID SSL CERTIFICATE', 'The certificate is invalid', ); case DioExceptionType.connectionError: return ( 'CONNECTION ERROR', 'Server cannot be reached', ); case DioExceptionType.unknown: return ( 'UNKNOWN ERROR', 'Possible cause may be a connection problem or an invalid SSL certificate', ); default: return ( 'UNKNOWN ERROR', 'Possible cause may be a connection problem or an invalid SSL certificate', ); } return ( 'UNKNOWN ERROR', 'Something unexpected went wrong', ); } Future ping() async { try { log.fine('Ping'); webdav.Client client = await connection; await client.ping(); } on DioException catch (e) { // Handle 204 No Content as success final statusCode = e.response?.statusCode; if (statusCode == 204) { log.fine('204 No Content (treated as success)'); return; } log.severe(e); final (String, String) error = _handleDioError(e); throw WebDAVClientException('${error.$1}: ${error.$2}'); } on Exception catch (e) { log.severe(e); throw const WebDAVClientException('Unknown Error: Something went wrong'); } } Future _exists({ required String path, required String target, }) async { if (!path.startsWith('/')) { path = '/$path'; } if (!path.endsWith('/')) { path = '$path/'; } for (webdav.File f in await listFiles(path: path)) { if (f.path == '$path$target') { return true; } } return false; } Future fileExists({ required String filename, }) async { if (filename.startsWith('/')) { filename = filename.substring(1); } if (filename.endsWith('/')) { filename = filename.substring(0, filename.length - 1); } log.fine('Check if file $filename exists'); return await _exists( path: p.dirname(filename), target: p.basename(filename), ); } Future getFile({ required String filename, }) async { webdav.Client client = await connection; try { log.fine('Read file object $filename'); return await client.readProps(filename); } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to read file object $filename from remote ${client.uri}', ); } } Future> listFiles({ String path = '', }) async { webdav.Client client = await connection; try { log.fine('List files and directories of ${path.isEmpty ? "/" : path}'); return await client.readDir(path); } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to list files in directory ${path.isEmpty ? "/" : path} on remote ${client.uri}', ); } } Future create(String filename) async { webdav.Client client = await connection; try { if (filename.startsWith('/')) { filename = filename.substring(1); } if (await fileExists(filename: filename) == false) { // Create file by writing empty string. log.fine('Create file $filename'); await client.write(filename, utf8.encode('')); } else { log.fine('Skip file creation. File $filename already exists'); } } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to create file $filename on remote ${client.uri}', ); } } Future mkdir( {required String directory, bool recursive = false}) async { webdav.Client client = await connection; if (directory.startsWith('/')) { directory = directory.substring(1); } if (directory.endsWith('/')) { directory = directory.substring(0, directory.length - 1); } try { log.fine('Create directory $directory'); if (recursive) { await client.mkdirAll(directory); } else { await client.mkdir(directory); } } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to create directory $directory on remote ${client.uri}', ); } } Future download({ required String filename, }) async { webdav.Client client = await connection; try { log.fine('Download content of file $filename'); List content = await client.read(filename); return utf8.decode(content); } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to download file $filename from remote ${client.uri}', ); } } Future upload({ required String filename, required String content, }) async { webdav.Client client = await connection; try { log.fine('Upload content to file $filename'); await client.write(filename, utf8.encode(content)); } on Exception catch (e) { log.severe(e); throw WebDAVClientException( 'Failed to upload file $filename to remote ${client.uri}', ); } } } ================================================ FILE: linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: linux/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.10) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "ntodotxt") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "de.tnmgl.ntodotxt") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) # Load bundled libraries from the lib/ directory relative to the binary. set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Root filesystem for cross-building. if(FLUTTER_TARGET_PLATFORM_SYSROOT) set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) endif() # Define build configuration options. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} "main.cc" "my_application.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) # Only the install-generated bundle's copy of the executable will launch # correctly, since the resources must in the right relative locations. To avoid # people trying to run the unbundled copy, put it in a subdirectory instead of # the default top-level location. set_target_properties(${BINARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" ) # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() # Start with a clean build bundle directory every time. install(CODE " file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") " COMPONENT Runtime) set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) install(FILES "${bundled_library}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endforeach(bundled_library) # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() ================================================ FILE: linux/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.10) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. # Serves the same purpose as list(TRANSFORM ... PREPEND ...), # which isn't available in 3.10. function(list_prepend LIST_NAME PREFIX) set(NEW_LIST "") foreach(element ${${LIST_NAME}}) list(APPEND NEW_LIST "${PREFIX}${element}") endforeach(element) set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) endfunction() # === Flutter Library === # System-level dependencies. find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "fl_basic_message_channel.h" "fl_binary_codec.h" "fl_binary_messenger.h" "fl_dart_project.h" "fl_engine.h" "fl_json_message_codec.h" "fl_json_method_codec.h" "fl_message_codec.h" "fl_method_call.h" "fl_method_channel.h" "fl_method_codec.h" "fl_method_response.h" "fl_plugin_registrar.h" "fl_plugin_registry.h" "fl_standard_message_codec.h" "fl_standard_method_codec.h" "fl_string_codec.h" "fl_value.h" "fl_view.h" "flutter_linux.h" ) list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") target_link_libraries(flutter INTERFACE PkgConfig::GTK PkgConfig::GLIB PkgConfig::GIO ) add_dependencies(flutter flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/_phony_ COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ) ================================================ FILE: linux/main.cc ================================================ #include "my_application.h" int main(int argc, char** argv) { g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } ================================================ FILE: linux/my_application.cc ================================================ #include "my_application.h" #include #ifdef GDK_WINDOWING_X11 #include #endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). // If running on X and not using GNOME then just use a traditional title bar // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); if (g_strcmp0(wm_name, "GNOME Shell") != 0) { use_header_bar = FALSE; } } #endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); gtk_header_bar_set_title(header_bar, "ntodotxt"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { gtk_window_set_title(window, "ntodotxt"); } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } // Implements GApplication::local_command_line. static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { MyApplication* self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; if (!g_application_register(application, nullptr, &error)) { g_warning("Failed to register: %s", error->message); *exit_status = 1; return TRUE; } g_application_activate(application); *exit_status = 0; return TRUE; } // Implements GApplication::startup. static void my_application_startup(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application startup. G_APPLICATION_CLASS(my_application_parent_class)->startup(application); } // Implements GApplication::shutdown. static void my_application_shutdown(GApplication* application) { //MyApplication* self = MY_APPLICATION(object); // Perform any actions required at application shutdown. G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); } // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_APPLICATION_CLASS(klass)->startup = my_application_startup; G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); } ================================================ FILE: linux/my_application.h ================================================ #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ #include G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication) /** * my_application_new: * * Creates a new Flutter-based application. * * Returns: a new #MyApplication. */ MyApplication* my_application_new(); #endif // FLUTTER_MY_APPLICATION_H_ ================================================ FILE: macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: macos/Flutter/Flutter-Debug.xcconfig ================================================ #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Flutter/Flutter-Release.xcconfig ================================================ #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = ntodotxt // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2025 de.tnmgl. All rights reserved. ================================================ FILE: macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server ================================================ FILE: macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox ================================================ FILE: macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC10EC2044A3C60003C045; remoteInfo = Runner; }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* ntodotxt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ntodotxt.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 331C80D2294CF70F00263BE5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C80D7294CF71000263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* ntodotxt.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C80D4294CF70F00263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, ); buildRules = ( ); dependencies = ( 331C80DA294CF71000263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* ntodotxt.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C80D3294CF70F00263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C80D1294CF70F00263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC10EC2044A3C60003C045 /* Runner */; targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = de.tnmgl.ntodotxt.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ntodotxt.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ntodotxt"; }; name = Profile; }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C80DB294CF71000263BE5 /* Debug */, 331C80DC294CF71000263BE5 /* Release */, 331C80DD294CF71000263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: macos/RunnerTests/RunnerTests.swift ================================================ import Cocoa import FlutterMacOS import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: metadata/en-US/full_description.txt ================================================ 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. ================================================ FILE: metadata/en-US/short_description.txt ================================================ Manage your todos in a todo.txt file ================================================ FILE: metadata/en-US/title.txt ================================================ ntodotxt ================================================ FILE: ntodotxt.yaml ================================================ session: ntodotxt root: ~/Projekte/dart/flutter/ntodotxt/ windows: - name: git - name: neovim - name: flutter run - name: flutter test ================================================ FILE: pubspec.yaml ================================================ name: ntodotxt description: App for managing your todos within a todo.txt file locally or via webdav (e.g. Nextcloud) publish_to: "none" version: 0.17.0+39 environment: sdk: ">=3.0.5 <4.0.0" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter collection: ^1.17.2 crypto: ^3.0.3 cupertino_icons: ^1.0.6 dio: ^5.9.0 equatable: ^2.0.5 file_picker: ^10.1.9 flex_color_scheme: ^8.0.2 flutter_bloc: ^9.1.1 flutter_secure_storage: ^9.0.0 go_router: ^15.1.2 intl: any logging: ^1.2.0 path: ^1.9.0 path_provider: ^2.0.11 permission_handler: ^12.0.0 rxdart: ^0.28.0 sqflite_common_ffi: ^2.3.1 sqlite3_flutter_libs: ^0.5.18 url_launcher: ^6.2.1 webdav_client: ^1.2.2 dev_dependencies: flutter_test: sdk: flutter integration_test: sdk: flutter flutter_driver: sdk: flutter file: ^7.0.0 flutter_launcher_icons: ^0.14.3 flutter_lints: ^5.0.0 flutter_oss_licenses: ^3.0.4 test_cov_console: ^0.2.2 flutter_launcher_icons: android: true image_path: assets/icon/icon.png min_sdk_android: 21 flutter: uses-material-design: true assets: - assets/icon/icon.png fonts: - family: OpenSans fonts: - asset: fonts/OpenSans-VariableFont_wdth,wght.ttf - asset: fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf ================================================ FILE: test/common/widget/confirm_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/confirm_dialog.dart'; class MaterialAppConfirmationDialog extends StatefulWidget { const MaterialAppConfirmationDialog({super.key}); @override State createState() => _MaterialAppConfirmationDialogState(); } class _MaterialAppConfirmationDialogState extends State { int _value = -1; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: [ Text('$_value'), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { bool confirm = await ConfirmationDialog.dialog( context: context, title: 'Dialog', message: 'Question?', actionLabel: 'Ok', cancelLabel: 'Cancel', ); setState(() { _value = confirm == true ? 1 : 0; }); }, ); }, ), ], ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ConfirmationDialog', () { testWidgets('confirm', (tester) async { await tester.pumpWidget(const MaterialAppConfirmationDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(AlertDialog), findsOneWidget); await tester.tap(find.text('Ok')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == '1', ), findsOneWidget, ); }); testWidgets('confirm', (tester) async { await tester.pumpWidget(const MaterialAppConfirmationDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(AlertDialog), findsOneWidget); await tester.tap(find.text('Cancel')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == '0', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/contexts_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Todo; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterContextTagDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterContextTagDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text( 'result: ${state.filter.contexts.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterContextTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { 'context1', 'context2', 'context3' }, ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } class MaterialAppTodoContextTagDialog extends StatelessWidget { const MaterialAppTodoContextTagDialog({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => TodoCubit( todo: Todo(description: 'Test something'), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, TodoState state) { return Column( children: [ Text( 'result: ${state.todo.contexts.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await TodoContextTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { 'context1', 'context2', 'context3' }, ); }, ); }, ), ], ); }, ), ), ); }, ), ); } } Future safeTapByFinder(WidgetTester tester, Finder finder) async { await tester.ensureVisible(finder); await tester.pumpAndSettle(); await tester.tap(finder); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterContextTagDialog', () { testWidgets('apply', (tester) async { await tester.pumpWidget(const MaterialAppFilterContextTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterContextTagDialog')), matching: find.text('context1'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {context1}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterContextTagDialog')), matching: find.text('context1'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); }); }); group('TodoContextTagDialog', () { testWidgets('enter', (tester) async { await tester.pumpWidget(const MaterialAppTodoContextTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); Finder textField = find.descendant( of: find.byKey(const Key('TodoContextTagDialog')), matching: find.byType(TextFormField), ); await tester.ensureVisible(textField); await tester.pumpAndSettle(); await tester.enterText(textField, 'context99'); await tester.pumpAndSettle(); await safeTapByFinder(tester, find.text('Add')); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('TodoContextTagDialog')), matching: find.text('context99'), ), findsOneWidget, ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {context99}', ), findsOneWidget, ); }); testWidgets('enter (with leading @)', (tester) async { await tester.pumpWidget(const MaterialAppTodoContextTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); Finder textField = find.descendant( of: find.byKey(const Key('TodoContextTagDialog')), matching: find.byType(TextFormField), ); await tester.ensureVisible(textField); await tester.pumpAndSettle(); await tester.enterText(textField, '@context99'); await tester.pumpAndSettle(); await safeTapByFinder(tester, find.text('Add')); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('TodoContextTagDialog')), matching: find.text('context99'), ), findsOneWidget, ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {context99}', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/default_filter_state_filter_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppDefaultFilterStateFilterDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppDefaultFilterStateFilterDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.filter.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await DefaultFilterStateFilterDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('DefaultFilterStateOrderDialog', () { testWidgets('change', (tester) async { await tester .pumpWidget(const MaterialAppDefaultFilterStateFilterDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Completed only')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'completedOnly', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('All')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'all', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/default_filter_state_group_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppDefaultFilterStateGroupDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppDefaultFilterStateGroupDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.group.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await DefaultFilterStateGroupDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('DefaultFilterStateGroupDialog', () { testWidgets('change', (tester) async { await tester.pumpWidget(const MaterialAppDefaultFilterStateGroupDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Upcoming')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'upcoming', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('None')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'none', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/default_filter_state_order_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppDefaultFilterStateOrderDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppDefaultFilterStateOrderDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.order.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await DefaultFilterStateOrderDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('DefaultFilterStateOrderDialog', () { testWidgets('change', (tester) async { await tester.pumpWidget(const MaterialAppDefaultFilterStateOrderDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Descending')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'descending', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Ascending')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'ascending', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/filter_state_filter_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterStateFilterDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterStateFilterDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.filter.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterStateFilterDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterStateFilterDialog', () { testWidgets('change', (tester) async { await tester.pumpWidget(const MaterialAppFilterStateFilterDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Completed only')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'completedOnly', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('All')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'all', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/filter_state_group_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterStateGroupDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterStateGroupDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.group.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterStateGroupDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterStateGroupDialog', () { testWidgets('change', (tester) async { await tester.pumpWidget(const MaterialAppFilterStateGroupDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Upcoming')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'upcoming', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('None')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'none', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/filter_state_order_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterStateOrderDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterStateOrderDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text(state.filter.order.name), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterStateOrderDialog.dialog( context: context, cubit: BlocProvider.of(context), ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterStateOrderDialog', () { testWidgets('change', (tester) async { await tester.pumpWidget(const MaterialAppFilterStateOrderDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Descending')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'descending', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(Dialog), findsOneWidget); await tester.tap(find.text('Ascending')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'ascending', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/info_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/info_dialog.dart'; class MaterialAppInfoDialog extends StatelessWidget { const MaterialAppInfoDialog({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: [ Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await InfoDialog.dialog( context: context, title: 'Dialog title', message: 'Dialog text', ); }, ); }, ), ], ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('InfoDialog', () { testWidgets('info', (tester) async { await tester.pumpWidget(const MaterialAppInfoDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(AlertDialog), findsOneWidget); expect( find.descendant( of: find.byType(AlertDialog), matching: find.text('Dialog title'), ), findsOneWidget, ); expect( find.descendant( of: find.byType(AlertDialog), matching: find.text('Dialog text'), ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/input_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/input_dialog.dart'; class MaterialAppInputDialog extends StatefulWidget { const MaterialAppInputDialog({super.key}); @override State createState() => _MaterialAppInputDialogState(); } class _MaterialAppInputDialogState extends State { String _value = 'default'; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: [ Text(_value), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { final String? value = await InputDialog.dialog( context: context, title: 'Dialog', label: 'Enter value', ); setState(() { _value = value ?? 'cancel'; }); }, ); }, ), ], ), ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('InputDialog', () { testWidgets('ok', (tester) async { await tester.pumpWidget(const MaterialAppInputDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(AlertDialog), findsOneWidget); await tester.enterText(find.byType(TextField), 'enter some text'); await tester.tap(find.text('Ok')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'enter some text', ), findsOneWidget, ); }); testWidgets('cancel', (tester) async { await tester.pumpWidget(const MaterialAppInputDialog()); await tester.pump(); await tester.tap(find.text('Open dialog')); await tester.pump(); expect(find.byType(AlertDialog), findsOneWidget); await tester.enterText(find.byType(TextField), 'enter some text'); await tester.tap(find.text('Cancel')); await tester.pump(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'cancel', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/key_values_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/key_values_dialog.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Todo; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; class MaterialAppTodoKeyValueTagDialog extends StatelessWidget { const MaterialAppTodoKeyValueTagDialog({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => TodoCubit( todo: Todo(description: 'Test something'), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, TodoState state) { return Column( children: [ Text( 'result: ${state.todo.keyValues.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await TodoKeyValueTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { 'key1:val1', 'key2:val2', 'key3:val3' }, ); }, ); }, ), ], ); }, ), ), ); }, ), ); } } Future safeTapByFinder(WidgetTester tester, Finder finder) async { await tester.ensureVisible(finder); await tester.pumpAndSettle(); await tester.tap(finder); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('TodoKeyValueTagDialog', () { testWidgets('enter', (tester) async { await tester.pumpWidget(const MaterialAppTodoKeyValueTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); Finder textField = find.descendant( of: find.byKey(const Key('TodoKeyValueTagDialog')), matching: find.byType(TextFormField), ); await tester.ensureVisible(textField); await tester.pumpAndSettle(); await tester.enterText(textField, 'foo:bar'); await tester.pumpAndSettle(); await safeTapByFinder(tester, find.text('Add')); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('TodoKeyValueTagDialog')), matching: find.text('foo:bar'), ), findsOneWidget, ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {foo:bar}', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/priorities_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority, Todo; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterPriorityTagDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterPriorityTagDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text( 'result: ${state.filter.priorities.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterPriorityTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { Priority.A, Priority.B, Priority.C }, ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } class MaterialAppTodoPriorityTagDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppTodoPriorityTagDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => TodoCubit( todo: Todo(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, TodoState state) { return Column( children: [ Text( 'result: ${state.todo.priority.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await TodoPriorityTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { Priority.A, Priority.B, Priority.C }, ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } Future safeTapByFinder(WidgetTester tester, Finder finder) async { await tester.ensureVisible(finder); await tester.pumpAndSettle(); await tester.tap(finder); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterPriorityTagDialog', () { testWidgets('set & unset', (tester) async { await tester.pumpWidget(const MaterialAppFilterPriorityTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterPriorityTagDialog')), matching: find.text('A'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {Priority.A}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterPriorityTagDialog')), matching: find.text('A'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); }); }); group('TodoPriorityTagDialog', () { testWidgets('set & unset', (tester) async { await tester.pumpWidget(const MaterialAppTodoPriorityTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: Priority.none', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('TodoPriorityTagDialog')), matching: find.text('A'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: Priority.A', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('TodoPriorityTagDialog')), matching: find.text('A'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: Priority.none', ), findsOneWidget, ); }); }); } ================================================ FILE: test/common/widget/projects_dialog_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Todo; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppFilterProjectTagDialog extends StatelessWidget { final DatabaseController dbController; const MaterialAppFilterProjectTagDialog({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: const Filter(), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, FilterState state) { return Column( children: [ Text( 'result: ${state.filter.projects.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await FilterProjectTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { 'project1', 'project2', 'project3' }, ); }, ); }, ), ], ); }, ), ), ); }, ), ), ); } } class MaterialAppTodoProjectTagDialog extends StatelessWidget { const MaterialAppTodoProjectTagDialog({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => TodoCubit( todo: Todo(description: 'Test something'), ), child: Builder( builder: (BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (BuildContext context, TodoState state) { return Column( children: [ Text( 'result: ${state.todo.projects.toString()}', ), Builder( builder: (BuildContext context) { return TextButton( child: const Text('Open dialog'), onPressed: () async { await TodoProjectTagDialog.dialog( context: context, cubit: BlocProvider.of(context), availableTags: { 'project1', 'project2', 'project3' }, ); }, ); }, ), ], ); }, ), ), ); }, ), ); } } Future safeTapByFinder(WidgetTester tester, Finder finder) async { await tester.ensureVisible(finder); await tester.pumpAndSettle(); await tester.tap(finder); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('FilterProjectTagDialog', () { testWidgets('apply', (tester) async { await tester.pumpWidget(const MaterialAppFilterProjectTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterProjectTagDialog')), matching: find.text('project1'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {project1}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); await safeTapByFinder( tester, find.descendant( of: find.byKey(const Key('FilterProjectTagDialog')), matching: find.text('project1'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); }); }); group('TodoProjectTagDialog', () { testWidgets('enter', (tester) async { await tester.pumpWidget(const MaterialAppTodoProjectTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); Finder textField = find.descendant( of: find.byKey(const Key('TodoProjectTagDialog')), matching: find.byType(TextFormField), ); await tester.ensureVisible(textField); await tester.pumpAndSettle(); await tester.enterText(textField, 'project99'); await tester.pumpAndSettle(); await safeTapByFinder(tester, find.text('Add')); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('TodoProjectTagDialog')), matching: find.text('project99'), ), findsOneWidget, ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {project99}', ), findsOneWidget, ); }); testWidgets('enter (with leading +)', (tester) async { await tester.pumpWidget(const MaterialAppTodoProjectTagDialog()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {}', ), findsOneWidget, ); await tester.tap(find.text('Open dialog')); await tester.pumpAndSettle(); Finder textField = find.descendant( of: find.byKey(const Key('TodoProjectTagDialog')), matching: find.byType(TextFormField), ); await tester.ensureVisible(textField); await tester.pumpAndSettle(); await tester.enterText(textField, '+project99'); await tester.pumpAndSettle(); await safeTapByFinder(tester, find.text('Add')); await tester.pumpAndSettle(); expect( find.descendant( of: find.byKey(const Key('TodoProjectTagDialog')), matching: find.text('project99'), ), findsOneWidget, ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == 'result: {project99}', ), findsOneWidget, ); }); }); } ================================================ FILE: test/drawer/state/drawer_cubit_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/drawer/state/drawer_cubit.dart'; import 'package:ntodotxt/drawer/state/drawer_state.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() async {}); group('initial', () { test('initial state', () { final DrawerCubit cubit = DrawerCubit(); expect(cubit.state, const DrawerState(index: 0)); }); }); group('next()', () { test('call one time next()', () { final DrawerCubit cubit = DrawerCubit(); cubit.next(9); expect(cubit.state, const DrawerState(index: 9, prevIndex: 0)); }); test('call two times next()', () { final DrawerCubit cubit = DrawerCubit(); cubit.next(9); cubit.next(2); expect(cubit.state, const DrawerState(index: 2, prevIndex: 9)); }); }); group('back()', () { test('call two times next() and one time back()', () { final DrawerCubit cubit = DrawerCubit(); cubit.next(9); cubit.next(2); cubit.back(); expect(cubit.state, const DrawerState(index: 9, prevIndex: null)); }); test('call two times next() and two time back()', () { final DrawerCubit cubit = DrawerCubit(); cubit.next(9); cubit.next(2); cubit.back(); cubit.back(); expect(cubit.state, const DrawerState(index: 0, prevIndex: null)); }); }); group('reset()', () { test('call two times next() and one time reset()', () { final DrawerCubit cubit = DrawerCubit(); cubit.next(9); cubit.next(2); cubit.reset(); expect(cubit.state, const DrawerState(index: 0, prevIndex: null)); }); }); group('toString()', () { test('default', () { const DrawerState state = DrawerState(index: 99, prevIndex: 0); expect( state.toString(), 'DrawerState { index: 99 prevIndex: 0 }', ); }); }); } ================================================ FILE: test/filter/controller/filter_controller_test.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart' show FilterController; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart' show FilterRepository; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); // Init ffi loader if needed. sqfliteFfiInit(); late DatabaseController controller; late FilterRepository repository; setUp(() async { controller = DatabaseController(inMemoryDatabasePath); repository = FilterRepository(FilterController(controller)); await (await controller.database).delete('filters'); // Clear }); group('list()', () { test('empty', () async { expect(await repository.list(), isEmpty); }); test('filled', () async { Filter model = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); await (await controller.database).insert('filters', model.toMap()); expect(await repository.list(), [model]); }); }); group('get()', () { test('empty', () async { expect(await repository.get(id: 1), null); }); test('filled', () async { Filter model = const Filter( name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); await (await controller.database).insert('filters', model.toMap()); expect(await repository.get(id: 1), model.copyWith(id: 1)); }); }); group('insert()', () { test('empty', () async { Filter model = const Filter( name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); expect(await repository.insert(model) > 0, isTrue); }); test('filled', () async { Filter model = const Filter( name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); await (await controller.database).insert('filters', model.toMap()); expect(await repository.insert(model) > 0, isTrue); }); test('ignore id', () async { Filter model = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); expect(await repository.insert(model) > 0, isTrue); }); }); group('update()', () { test('empty', () async { Filter model = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); expect(await repository.update(model), 0); }); test('filled', () async { Filter model1 = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); Filter model2 = model1.copyWith(name: 'updated name'); await (await controller.database).insert('filters', model1.toMap()); expect(await repository.update(model2) > 0, isTrue); }); test('missing id', () async { Filter model = const Filter( name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); expect(await repository.update(model), 0); }); }); group('delete()', () { test('empty', () async { Filter model = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); expect(await repository.delete(id: model.id!), 0); }); test('filled', () async { Filter model = const Filter( id: 1, name: 'example filter', priorities: {Priority.A, Priority.B}, projects: {'project1', 'project2'}, contexts: {'context1', 'context2'}, order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ); await (await controller.database).insert('filters', model.toMap()); expect(await repository.delete(id: model.id!) > 0, isTrue); }); }); } ================================================ FILE: test/filter/page/filter_create_edit_page_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/page/filter_create_edit_page.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class BlocRepositoryWrapper extends StatelessWidget { final Filter? initFilter; final Set projects; final Set contexts; const BlocRepositoryWrapper({ this.initFilter, this.projects = const {}, this.contexts = const {}, super.key, }); @override Widget build(BuildContext context) { final DatabaseController dbController = DatabaseController(inMemoryDatabasePath); return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: MaterialApp( home: FilterCreateEditPage( initFilter: initFilter, projects: projects, contexts: contexts, ), ), ); } } void main() { group('FilterCreateEditPage', () { group('narrow view', () { group('create mode', () { testWidgets('found no SaveFilterIconButton if name is empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveFilterIconButton if name is not empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'Filter name'); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no DeleteFilterIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); expect( find.byType(DeleteFilterIconButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); group('edit mode', () { testWidgets( 'found no SaveFilterIconButton if filter has not be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveFilterIconButton if filter has be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterOrderItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterOrderItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateOrderDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterStateOrderDialog), matching: find.text('Descending'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterOrderItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListOrder.descending.name, ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DeleteFilterIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); expect( find.byType(DeleteFilterIconButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); }); group('wide view', () { group('create mode', () { testWidgets('found no SaveFilterIconButton if name is empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveFilterIconButton if name is not empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'Filter name'); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no DeleteFilterIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); expect( find.byType(DeleteFilterIconButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); group('edit mode', () { testWidgets( 'found no SaveFilterIconButton if filter has not be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveFilterIconButton if filter has be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterOrderItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterOrderItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateOrderDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterStateOrderDialog), matching: find.text('Descending'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterOrderItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListOrder.descending.name, ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(SaveFilterIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DeleteFilterIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter'), ), ); await tester.pumpAndSettle(); expect( find.byType(DeleteFilterIconButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); }); group('default values', () { testWidgets('FilterOrderItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterOrderItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterOrderItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListOrder.ascending.name, ), ), findsOneWidget, ); }); testWidgets('FilterFilterItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterFilterItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterFilterItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListFilter.all.name, ), ), findsOneWidget, ); }); testWidgets('FilterGroupItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterGroupItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterGroupItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListGroup.none.name, ), ), findsOneWidget, ); }); testWidgets('FilterPrioritiesItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterPrioritiesItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterPrioritiesItem), matching: find.byType(BasicChip), ), findsNothing, ); expect( find.descendant( of: find.byType(FilterPrioritiesItem), matching: find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == '-', ), ), findsOneWidget, ); }); testWidgets('FilterProjectTagsItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterProjectTagsItem), matching: find.byType(BasicChip), ), findsNothing, ); expect( find.descendant( of: find.byType(FilterProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == '-', ), ), findsOneWidget, ); }); testWidgets('FilterContextTagsItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterContextTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterContextTagsItem), matching: find.byType(BasicChip), ), findsNothing, ); expect( find.descendant( of: find.byType(FilterContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data == '-', ), ), findsOneWidget, ); }); }); group('non default values', () { testWidgets('FilterNameTextField', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(name: 'filter name'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterNameTextField), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterNameTextField), matching: find.text('filter name'), ), findsOneWidget, ); }); testWidgets('FilterOrderItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(order: ListOrder.descending), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterOrderItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterOrderItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListOrder.descending.name, ), ), findsOneWidget, ); }); testWidgets('FilterFilterItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(filter: ListFilter.completedOnly), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterFilterItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterFilterItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListFilter.completedOnly.name, ), ), findsOneWidget, ); }); testWidgets('FilterGroupItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith(group: ListGroup.project), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterGroupItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterGroupItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListGroup.project.name, ), ), findsOneWidget, ); }); testWidgets('FilterPrioritiesItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith( priorities: {Priority.A, Priority.B}, ), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterPrioritiesItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterPrioritiesItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == Priority.A.name, ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(FilterPrioritiesItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == Priority.B.name, ), ), findsOneWidget, ); }); testWidgets('FilterProjectTagsItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith( projects: {'project1', 'project2'}, ), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project1', ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(FilterProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project2', ), ), findsOneWidget, ); }); testWidgets('FilterContextTagsItem', (tester) async { await tester.pumpWidget( BlocRepositoryWrapper( initFilter: const Filter().copyWith( contexts: {'context1', 'context2'}, ), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterContextTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(FilterContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context1', ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(FilterContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context2', ), ), findsOneWidget, ); }); }); group('update values', () { testWidgets('FilterOrderItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterOrderItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterOrderItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateOrderDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterStateOrderDialog), matching: find.text('Descending'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterOrderItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListOrder.descending.name, ), ), findsOneWidget, ); }); testWidgets('FilterFilterItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterFilterItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterFilterItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateFilterDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterStateFilterDialog), matching: find.text('Completed only'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterFilterItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListFilter.completedOnly.name, ), ), findsOneWidget, ); }); testWidgets('FilterGroupItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterGroupItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterGroupItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateGroupDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterStateGroupDialog), matching: find.text('Upcoming'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterGroupItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == ListGroup.upcoming.name, ), ), findsOneWidget, ); }); testWidgets('FilterPrioritiesItem', (tester) async { await tester.pumpWidget(const BlocRepositoryWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterPrioritiesItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterPrioritiesItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterPriorityTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterPriorityTagDialog), matching: find.text('A'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterPrioritiesItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'A', ), ), findsOneWidget, ); }); testWidgets('FilterProjectTagsItem', (tester) async { await tester.pumpWidget( const BlocRepositoryWrapper( projects: {'project1', 'project2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterProjectTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterProjectTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterProjectTagDialog), matching: find.text('project1'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project1', ), ), findsOneWidget, ); }); testWidgets('FilterContextTagsItem', (tester) async { await tester.pumpWidget( const BlocRepositoryWrapper( contexts: {'context1', 'context2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(FilterContextTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(FilterContextTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterContextTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(FilterContextTagDialog), matching: find.text('context1'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(FilterContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context1', ), ), findsOneWidget, ); }); }); }); } ================================================ FILE: test/filter/page/filter_list_page_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter; import 'package:ntodotxt/filter/page/filter_list_page.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingController; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class FakeController extends Fake implements FilterController { List items = [ const Filter(id: 1, name: 'Filter 01'), const Filter(id: 2, name: 'Filter 02'), const Filter(id: 3, name: 'Filter 03'), ]; @override Future> list() async { return Future.value(items); } } class FilterListPageMaterialApp extends StatelessWidget { final DatabaseController dbController = const DatabaseController(inMemoryDatabasePath); final FilterController controller; const FilterListPageMaterialApp({ required this.controller, super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository(SettingController(dbController)), ), RepositoryProvider( create: (BuildContext context) => FilterRepository(controller), ), BlocProvider( create: (BuildContext context) => InteractionSettingsCubit( repository: context.read(), ), ), ], child: Builder( builder: (BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) { return FilterListBloc( repository: context.read(), ) ..add(const FilterListSubscriped()) ..add(const FilterListSynchronizationRequested()); }, ), ], child: const MaterialApp( home: FilterListPage(), ), ); }, ), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('List', () { testWidgets('narrow view', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(FilterListPageMaterialApp( controller: FakeController(), )); await tester.pump(); expect(find.byType(FilterListTile), findsNWidgets(3)); Iterable filterTiles = tester.widgetList(find.byType(FilterListTile)); expect(filterTiles.elementAt(0).filter.name, 'Filter 01'); expect(filterTiles.elementAt(1).filter.name, 'Filter 02'); expect(filterTiles.elementAt(2).filter.name, 'Filter 03'); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('wide view', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 1200); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(FilterListPageMaterialApp( controller: FakeController(), )); await tester.pump(); expect(find.byType(FilterListTile), findsNWidgets(3)); Iterable filterTiles = tester.widgetList(find.byType(FilterListTile)); expect(filterTiles.elementAt(0).filter.name, 'Filter 01'); expect(filterTiles.elementAt(1).filter.name, 'Filter 02'); expect(filterTiles.elementAt(2).filter.name, 'Filter 03'); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); } ================================================ FILE: test/filter/state/filter_cubit_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter, ListFilter, ListGroup, ListOrder; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart' show Priority; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late DatabaseController controller; late SettingRepository settingRepository; late FilterRepository filterRepository; setUp(() { controller = DatabaseController(inMemoryDatabasePath); settingRepository = SettingRepository( SettingController(controller), ); filterRepository = FilterRepository( FilterController(controller), ); }); group('saved filter', () { group('initial', () { test('initial filter', () async { const Filter origin = Filter(name: 'default'); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); expect( cubit.state, FilterSaved( filter: const Filter( name: 'default', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), origin: origin, ), ); }); }); group('create filter', () { test('non-existing', () async { const Filter origin = Filter(name: 'default'); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.create(origin.copyWith(name: 'created')); await expectLater( cubit.state, FilterSaved( filter: const Filter( id: 1, name: 'created', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), origin: const Filter( id: 1, name: 'created', order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), ), ); }); }); // @todo: Fix testcase // group('update filter', () { // test('existing', () async { // const Filter origin = Filter(name: 'default'); // final FilterCubit cubit = FilterCubit( // settingRepository: settingRepository, // filterRepository: filterRepository, // filter: origin, // ); // await cubit.create(origin); // await cubit.update(origin.copyWith(id: 1, name: 'updated')); // // await expectLater( // cubit.state, // FilterSaved( // filter: const Filter( // id: 1, // name: 'updated', // order: ListOrder.ascending, // filter: ListFilter.all, // group: ListGroup.none, // ), // origin: const Filter( // id: 1, // name: 'updated', // order: ListOrder.ascending, // filter: ListFilter.all, // group: ListGroup.none, // ), // ), // ); // }); // }); group('delete filter', () { test('existing', () async { const Filter origin = Filter(name: 'default'); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.create(origin); await cubit.delete(origin.copyWith(id: 1, name: 'deleted')); await expectLater( cubit.state, FilterSaved( filter: const Filter(), origin: const Filter(), ), ); }); }); group('update attributes', () { test('name', () async { const Filter origin = Filter(name: 'default'); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateName('update'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(name: 'update'), origin: origin, ), ); }); test('order', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateOrder(ListOrder.descending); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(order: ListOrder.descending), origin: origin, ), ); }); test('filter', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateFilter(ListFilter.completedOnly); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(filter: ListFilter.completedOnly), origin: origin, ), ); }); test('group', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateGroup(ListGroup.priority); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(group: ListGroup.priority), origin: origin, ), ); }); }); group('priority', () { test('add', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addPriority(Priority.A); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(priorities: {Priority.A}), origin: origin, ), ); }); test('add (already exists)', () async { const Filter origin = Filter(priorities: {Priority.A}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addPriority(Priority.A); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(priorities: {Priority.A}), origin: origin, ), ); }); test('remove', () async { const Filter origin = Filter(priorities: {Priority.A}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removePriority(Priority.A); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(priorities: {}), origin: origin, ), ); }); test('remove (not exists)', () async { const Filter origin = Filter(priorities: {Priority.A}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removePriority(Priority.B); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(priorities: {Priority.A}), origin: origin, ), ); }); test('update multiple', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updatePriorities({Priority.A, Priority.B}); expect( cubit.state, FilterChanged( filter: const Filter().copyWith( priorities: {Priority.A, Priority.B}, ), origin: origin, ), ); }); }); group('project', () { test('add', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addProject('project1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(projects: {'project1'}), origin: origin, ), ); }); test('add (already exists)', () async { const Filter origin = Filter(projects: {'project1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addProject('project1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(projects: {'project1'}), origin: origin, ), ); }); test('remove', () async { const Filter origin = Filter(projects: {'project1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removeProject('project1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(projects: {}), origin: origin, ), ); }); test('remove (not exists)', () async { const Filter origin = Filter(projects: {'project1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removeProject('project2'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(projects: {'project1'}), origin: origin, ), ); }); test('update multiple', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateProjects({'project1', 'project2'}); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(projects: {'project1', 'project2'}), origin: origin, ), ); }); }); group('context', () { test('add', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addContext('context1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(contexts: {'context1'}), origin: origin, ), ); }); test('add (already exists)', () async { const Filter origin = Filter(contexts: {'context1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.addContext('context1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(contexts: {'context1'}), origin: origin, ), ); }); test('remove', () async { const Filter origin = Filter(contexts: {'context1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removeContext('context1'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(contexts: {}), origin: origin, ), ); }); test('remove (not exists)', () async { const Filter origin = Filter(contexts: {'context1'}); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.removeContext('context2'); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(contexts: {'context1'}), origin: origin, ), ); }); test('update multiple', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); cubit.updateContexts({'context1', 'context2'}); expect( cubit.state, FilterChanged( filter: const Filter().copyWith(contexts: {'context1', 'context2'}), origin: origin, ), ); }); }); }); group('default filter', () { group('update', () { test('order', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.updateDefaultOrder(ListOrder.descending); expect( cubit.state, FilterSaved( filter: const Filter().copyWith(order: ListOrder.descending), origin: const Filter().copyWith(order: ListOrder.descending), ), ); }); test('filter', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.updateDefaultFilter(ListFilter.completedOnly); expect( cubit.state, FilterSaved( filter: const Filter().copyWith(filter: ListFilter.completedOnly), origin: const Filter().copyWith(filter: ListFilter.completedOnly), ), ); }); test('group', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.updateDefaultGroup(ListGroup.priority); expect( cubit.state, FilterSaved( filter: const Filter().copyWith(group: ListGroup.priority), origin: const Filter().copyWith(group: ListGroup.priority), ), ); }); }); group('reset', () { test('full', () async { const Filter origin = Filter(); final FilterCubit cubit = FilterCubit( settingRepository: settingRepository, filterRepository: filterRepository, filter: origin, ); await cubit.updateDefaultOrder(ListOrder.descending); await cubit.updateDefaultFilter(ListFilter.completedOnly); await cubit.updateDefaultGroup(ListGroup.priority); expect( cubit.state, FilterSaved( filter: const Filter( order: ListOrder.descending, filter: ListFilter.completedOnly, group: ListGroup.priority, ), origin: const Filter( order: ListOrder.descending, filter: ListFilter.completedOnly, group: ListGroup.priority, ), ), ); await cubit.resetToDefaults(); expect( cubit.state, FilterSaved( filter: origin, origin: origin, ), ); }); }); }); } ================================================ FILE: test/filter/widget/filter_chip_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/filter_dialog.dart'; import 'package:ntodotxt/common/widget/group_by_dialog.dart'; import 'package:ntodotxt/common/widget/order_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart'; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_state.dart'; import 'package:ntodotxt/filter/widget/filter_chip.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class BlocAppWrapper extends StatelessWidget { final Widget child; final Filter filter; final DatabaseController dbController = const DatabaseController(inMemoryDatabasePath); const BlocAppWrapper({ required this.child, required this.filter, super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: filter, ), child: Builder( builder: (BuildContext context) { return BlocBuilder( builder: (BuildContext context, FilterState state) { return MaterialApp(home: Scaffold(body: child)); }, ); }, ), ), ); } } void main() { group('FilterOrderChip', () { testWidgets('ascending', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( order: ListOrder.ascending, ), child: FilterOrderChip(), ), ); await tester.pumpAndSettle(); expect(find.text('asc'), findsOneWidget); expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('descending', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( order: ListOrder.descending, ), child: FilterOrderChip(), ), ); await tester.pumpAndSettle(); expect(find.text('desc'), findsOneWidget); expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('ascending changed', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( order: ListOrder.ascending, ), child: FilterOrderChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterOrderChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateOrderDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('Descending')); await tester.pumpAndSettle(); expect(find.text('desc'), findsOneWidget); expect(find.byIcon(Icons.keyboard_arrow_down), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); testWidgets('descending changed', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( order: ListOrder.descending, ), child: FilterOrderChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterOrderChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateOrderDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('Ascending')); await tester.pumpAndSettle(); expect(find.text('asc'), findsOneWidget); expect(find.byIcon(Icons.keyboard_arrow_up), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); group('FilterFilterChip', () { testWidgets('all', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( filter: ListFilter.all, ), child: FilterFilterChip(), ), ); await tester.pumpAndSettle(); expect(find.text('all'), findsOneWidget); expect(find.byIcon(Icons.filter_list), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('completed', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( filter: ListFilter.completedOnly, ), child: FilterFilterChip(), ), ); await tester.pumpAndSettle(); expect(find.text('completed'), findsOneWidget); expect(find.byIcon(Icons.done_all), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('incompleted', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( filter: ListFilter.incompletedOnly, ), child: FilterFilterChip(), ), ); await tester.pumpAndSettle(); expect(find.text('incompleted'), findsOneWidget); expect(find.byIcon(Icons.remove_done), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('update', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( filter: ListFilter.completedOnly, ), child: FilterFilterChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterFilterChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateFilterDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('All')); await tester.pumpAndSettle(); expect(find.text('all'), findsOneWidget); expect(find.byIcon(Icons.filter_list), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); group('FilterGroupChip', () { testWidgets('none', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.none, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); expect(find.text('none'), findsOneWidget); expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('upcoming', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.upcoming, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); expect(find.text('upcoming'), findsOneWidget); expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('priority', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.priority, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); expect(find.text('priority'), findsOneWidget); expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('project', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.project, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); expect(find.text('project'), findsOneWidget); expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('context', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.context, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); expect(find.text('context'), findsOneWidget); expect(find.byIcon(Icons.workspaces_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('update', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( group: ListGroup.none, ), child: FilterGroupChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterGroupChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterStateGroupDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('Priority')); await tester.pumpAndSettle(); expect(find.text('priority'), findsOneWidget); expect(find.byIcon(Icons.workspaces), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); group('FilterPrioritiesChip', () { testWidgets('default', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( priorities: {}, ), child: FilterPrioritiesChip(), ), ); await tester.pumpAndSettle(); expect(find.text('priorities'), findsOneWidget); expect(find.byIcon(Icons.flag_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('update', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( priorities: {Priority.A}, ), child: FilterPrioritiesChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterPrioritiesChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterPriorityTagDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('A')); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect(find.text('priorities'), findsOneWidget); expect(find.byIcon(Icons.flag), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); group('FilterProjectsChip', () { testWidgets('default', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( projects: {}, ), child: FilterProjectsChip(), ), ); await tester.pumpAndSettle(); expect(find.text('projects'), findsOneWidget); expect(find.byIcon(Icons.rocket_launch_outlined), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('update', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( projects: {'project1', 'project2'}, ), child: FilterProjectsChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterProjectsChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterProjectTagDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('project2')); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect(find.text('projects'), findsOneWidget); expect(find.byIcon(Icons.rocket_launch), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); group('FilterContextsChip', () { testWidgets('default', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( contexts: {}, ), child: FilterContextsChip(), ), ); await tester.pumpAndSettle(); expect(find.text('contexts'), findsOneWidget); expect(find.byIcon(Icons.tag), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == false), findsOneWidget, ); }); testWidgets('update', (tester) async { await tester.pumpWidget( const BlocAppWrapper( filter: Filter( contexts: {'context1', 'context2'}, ), child: FilterContextsChip(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(FilterContextsChip)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(FilterContextTagDialog)); await tester.pumpAndSettle(); await tester.tap(find.text('context2')); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect(find.text('contexts'), findsOneWidget); expect(find.byIcon(Icons.tag), findsOneWidget); expect( find.byWidgetPredicate((Widget widget) => widget is GenericActionChip && widget.selected == true), findsOneWidget, ); }); }); } ================================================ FILE: test/login/page/webdav_login_view_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/login/page/login_page.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class MaterialAppWebDAVLoginView extends StatelessWidget { final DatabaseController dbController; const MaterialAppWebDAVLoginView({ this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), ], child: BlocProvider( create: (BuildContext context) => TodoFileCubit( repository: context.read(), state: TodoFileReady(), )..load(), child: Builder( builder: (BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, home: WebDAVLoginView(), ); }, ), ), ); } } void main() { group('Login form validation', () { group('success', () { testWidgets('Render form', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); expect(find.byType(WebDAVLoginView), findsOneWidget); }); testWidgets('Server address with http', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'http://localhost'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsNothing, ); }); testWidgets('Server address with https', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'https://localhost'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsNothing, ); }); testWidgets('Server address with port', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'http://localhost:80'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsNothing, ); }); testWidgets('Server address with dots', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'http://localhost.local'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsNothing, ); }); }); group('failed', () { testWidgets('Missing path', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.text('Missing path'), findsOneWidget); }); testWidgets('Missing username', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.text('Missing username'), findsOneWidget); }); testWidgets('Missing password', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.text('Missing password'), findsOneWidget); }); testWidgets('Missing server address', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect(find.text('Missing server address'), findsOneWidget); }); testWidgets('Missing protocol', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'localhost'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Missing protocol'), ), findsOneWidget, ); }); testWidgets('Missing server port (http)', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'http://localhost:'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsOneWidget, ); }); testWidgets('Missing server port (https)', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'https://localhost:'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsOneWidget, ); }); testWidgets('Invalid format (port is string)', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText( formField, 'https://localhost:abc', ); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsOneWidget, ); }); testWidgets('Invalid format (multiple ports)', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText( formField, 'https://localhost:80:90', ); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsOneWidget, ); }); testWidgets('Invalid format (invalid host)', (tester) async { await tester.pumpWidget(const MaterialAppWebDAVLoginView()); Finder formField = find.ancestor( of: find.text('Server'), matching: find.byType(TextFormField), ); await tester.enterText(formField, 'https://local host:80'); await tester.tap(find.text('Apply')); await tester.pumpAndSettle(); expect( find.descendant( of: formField, matching: find.text('Invalid format'), ), findsOneWidget, ); }); }); }); } ================================================ FILE: test/setting/controller/setting_controller_test.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingController; import 'package:ntodotxt/setting/model/setting_model.dart' show Setting; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); // Init ffi loader if needed. sqfliteFfiInit(); late DatabaseController controller; late SettingRepository repository; setUp(() async { controller = DatabaseController(inMemoryDatabasePath); repository = SettingRepository(SettingController(controller)); await (await controller.database).delete('settings'); // Clear }); group('list()', () { test('empty', () async { expect(await repository.list(), isEmpty); }); test('filled', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); await (await controller.database).insert('settings', model.toMap()); expect(await repository.list(), [model]); }); }); group('get()', () { test('empty', () async { expect(await repository.get(key: 'key1'), null); }); test('filled', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); await (await controller.database).insert('settings', model.toMap()); expect(await repository.get(key: 'key1'), model); }); }); group('insert()', () { test('empty', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); expect(await repository.insert(model) > 0, isTrue); }); test('filled', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); await (await controller.database).insert('settings', model.toMap()); expect(await repository.insert(model), 0); }); }); group('update()', () { test('empty', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); expect(await repository.update(model), 0); }); test('filled', () async { Setting model1 = const Setting( key: 'key1', value: 'value1', ); Setting model2 = model1.copyWith(value: 'value2'); await (await controller.database).insert('settings', model1.toMap()); expect(await repository.update(model2) > 0, isTrue); }); }); group('updateOrInsert()', () { test('empty', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); expect(await repository.updateOrInsert(model) > 0, isTrue); }); test('filled', () async { Setting model1 = const Setting( key: 'key1', value: 'value1', ); Setting model2 = model1.copyWith(value: 'value2'); await (await controller.database).insert('settings', model1.toMap()); expect(await repository.updateOrInsert(model2) > 0, isTrue); }); }); group('delete()', () { test('empty', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); expect(await repository.delete(key: model.key), 0); }); test('filled', () async { Setting model = const Setting( key: 'key1', value: 'value1', ); await (await controller.database).insert('settings', model.toMap()); expect(await repository.delete(key: model.key) > 0, isTrue); }); }); } ================================================ FILE: test/setting/page/settings_page_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter, ListFilter, ListGroup, ListOrder; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/login/state/login_cubit.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingController; import 'package:ntodotxt/setting/page/settings_page.dart' show SettingsPage; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class SettingsPageBlocProvider extends StatelessWidget { final DatabaseController dbController; final Filter? filter; const SettingsPageBlocProvider({ this.filter, this.dbController = const DatabaseController(inMemoryDatabasePath), super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ BlocProvider( create: (BuildContext context) => LoginCubit(), ), RepositoryProvider( create: (BuildContext context) => SettingRepository( SettingController(dbController), ), ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), BlocProvider( create: (BuildContext context) => InteractionSettingsCubit( repository: context.read(), ), ), ], child: MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: filter ?? const Filter(), )..load(), ), BlocProvider( create: (BuildContext context) => TodoFileCubit( repository: context.read(), )..load(), ), ], child: Builder( builder: (BuildContext context) { return const MaterialApp( home: SettingsPage(), ); }, ), ), ); } } void main() { group('Display settings', () { group('order', () { testWidgets('default value', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default order' && (widget.subtitle as Text).data == ListOrder.ascending.name, ), findsOneWidget, ); }); testWidgets('update by dialog', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); await tester.tap( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default order' && (widget.subtitle as Text).data == ListOrder.ascending.name, ), ); await tester.pumpAndSettle(); expect(find.byType(Dialog), findsOneWidget); await tester.tap( find.byKey(Key('${ListOrder.descending.name}DialogRadioButton')), ); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default order' && (widget.subtitle as Text).data == ListOrder.descending.name, ), findsOneWidget, ); }); }); group('filter', () { testWidgets('default value', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default filter' && (widget.subtitle as Text).data == ListFilter.all.name, ), findsOneWidget, ); }); testWidgets('update by dialog', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); await tester.tap( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default filter' && (widget.subtitle as Text).data == ListFilter.all.name, ), ); await tester.pumpAndSettle(); expect(find.byType(Dialog), findsOneWidget); await tester.tap( find.byKey(Key('${ListFilter.completedOnly.name}DialogRadioButton')), ); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default filter' && (widget.subtitle as Text).data == ListFilter.completedOnly.name, ), findsOneWidget, ); }); }); group('group by', () { testWidgets('default value', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default grouping' && (widget.subtitle as Text).data == ListGroup.none.name, ), findsOneWidget, ); }); testWidgets('update by dialog', (tester) async { await tester.pumpWidget(const SettingsPageBlocProvider()); await tester.pumpAndSettle(); await tester.tap( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default grouping' && (widget.subtitle as Text).data == ListGroup.none.name, ), ); await tester.pumpAndSettle(); expect(find.byType(Dialog), findsOneWidget); await tester.tap( find.byKey(Key('${ListGroup.priority.name}DialogRadioButton')), ); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ListTile && (widget.title as Text).data == 'Default grouping' && (widget.subtitle as Text).data == ListGroup.priority.name, ), findsOneWidget, ); }); }); }); } ================================================ FILE: test/todo/api/todo_list_api_test.dart ================================================ import 'dart:io'; import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/repository/todo_list_repository.dart'; File mockTodoListFile(List rawTodoList) { final MemoryFileSystem fs = MemoryFileSystem(); final File file = fs.file('todo.txt'); file.createSync(); file.writeAsStringSync( rawTodoList.join(Platform.lineTerminator), flush: true, ); return file; } TodoListRepository mockLocalTodoListRepository(File todoFile) { final LocalTodoListApi api = LocalTodoListApi.fromFile(localFile: todoFile); final TodoListRepository repository = TodoListRepository(api); return repository; } void main() { late File todoFile; late TodoListRepository repository; setUp(() { todoFile = mockTodoListFile([]); repository = mockLocalTodoListRepository(todoFile); }); group('LocalTodoListApi', () { group('init()', () { test('initial file with initial todo', () async { const List todoListStr = [ '2023-11-23 Code something', ]; todoFile = mockTodoListFile(todoListStr); repository = mockLocalTodoListRepository(todoFile); await expectLater( repository.getTodoList(), emitsInOrder( [ [for (var s in todoListStr) Todo.fromString(value: s)], ], ), ); }); test('initial file with multiple initial todos', () async { List todoListStr = [ 'x 2023-12-03 2023-12-02 TodoA', '1970-01-01 TodoB due:1970-01-01', '2023-12-02 TodoC due:2023-12-04', '2023-12-02 TodoD due:2023-12-05', '2023-11-11 TodoE', ]; todoFile = mockTodoListFile(todoListStr); repository = mockLocalTodoListRepository(todoFile); await expectLater( repository.getTodoList(), emitsInOrder( [ [for (var s in todoListStr) Todo.fromString(value: s)], ], ), ); }); }); group('read and write', () { test('readFromSource()', () async { const List todoListStr = [ '2023-11-23 Code something', ]; expect(await todoFile.readAsLines(), []); todoFile.writeAsStringSync( todoListStr.join(Platform.lineTerminator), flush: true, ); await repository.readFromSource(); expect(await todoFile.readAsLines(), todoListStr); }); test('writeToSource()', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); repository.saveTodo(todo); await repository.writeToSource(); expect(await todoFile.readAsLines(), [todo.toString()]); }); }); group('existsTodo()', () { test('existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); repository.saveTodo(todo); await expectLater( repository.getTodoList(), emitsInOrder([ [todo], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), [todo.toString()]); expect(repository.existsTodo(todo), true); }); test('non-existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); expect(await todoFile.readAsLines(), []); expect(repository.existsTodo(todo), false); }); }); group('saveTodo()', () { test('create new todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); repository.saveTodo(todo); await expectLater( repository.getTodoList(), emitsInOrder([ [todo], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), [todo.toString()]); }); test('update existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '1', value: '2023-11-23 Code something other', ); repository.saveTodo(todo); repository.saveTodo(todo2); // Update existing one. await expectLater( repository.getTodoList(), emitsInOrder([ [todo2], ]), ); await repository.writeToSource(); expect( await todoFile.readAsLines(), [todo2.toString()], ); }); test('update/save non-existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '2', value: '2023-11-23 Code something other', ); repository.saveTodo(todo); repository.saveTodo(todo2); // Update non-existing one. await expectLater( repository.getTodoList(), emitsInOrder([ [todo, todo2], ]), ); await repository.writeToSource(); expect( await todoFile.readAsLines(), [todo.toString(), todo2.toString()]); }); }); group('deleteTodo()', () { test('delete existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); repository.saveTodo(todo); repository.deleteTodo(todo); await expectLater( repository.getTodoList(), emitsInOrder([ [], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), []); }); test('delete non-existing todo', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '2', value: '2023-11-23 Code something other', ); repository.saveTodo(todo); repository.deleteTodo(todo2); // Delete non-existing todo. await expectLater( repository.getTodoList(), emitsInOrder([ [todo], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), [todo.toString()]); }); }); group('saveMultipleTodos()', () { test('create and update todos', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '2', value: '2023-11-23 Code something other', ); repository.saveTodo(todo); repository.saveTodo(todo2); await expectLater( repository.getTodoList(), emitsInOrder([ [todo, todo2], ]), ); await repository.writeToSource(); expect( await todoFile.readAsLines(), [ todo.toString(), todo2.toString(), ], ); final Todo todoUpdate = todo.copyWith( completion: true, completionDate: DateTime.now(), ); final Todo todo2Update = todo2.copyWith( completion: true, completionDate: DateTime.now(), ); repository.saveMultipleTodos([todoUpdate, todo2Update]); await expectLater( repository.getTodoList(), emitsInOrder( [ [todoUpdate, todo2Update], ], ), ); await repository.writeToSource(); expect( await todoFile.readAsLines(), [ todoUpdate.toString(), todo2Update.toString(), ], ); }); }); group('deleteMultipleTodos()', () { test('delete todos', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '2', value: '2023-11-23 Code something other', ); repository.saveTodo(todo); repository.saveTodo(todo2); await expectLater( repository.getTodoList(), emitsInOrder([ [todo, todo2], ]), ); await repository.writeToSource(); expect( await todoFile.readAsLines(), [ todo.toString(), todo2.toString(), ], ); repository.deleteMultipleTodos([todo, todo2]); await expectLater( repository.getTodoList(), emitsInOrder([ [], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), []); }); test('delete non-existing todos', () async { final Todo todo = Todo.fromString( id: '1', value: '2023-11-23 Code something', ); final Todo todo2 = Todo.fromString( id: '2', value: '2023-11-23 Code something other', ); repository.deleteMultipleTodos([todo, todo2]); await expectLater( repository.getTodoList(), emitsInOrder([ [], ]), ); await repository.writeToSource(); expect(await todoFile.readAsLines(), []); }); }); }); } ================================================ FILE: test/todo/model/todo_model_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; void main() { setUp(() {}); group('todo Todo()', () { group('completion & completionDate', () { test('initial incompleted', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.completion, false); expect(todo.completionDate, null); }); test('initial completed', () { final DateTime now = DateTime.now(); final Todo todo = Todo( completion: true, description: 'Write some tests', ); expect(todo.completion, true); expect(todo.completionDate, DateTime(now.year, now.month, now.day)); }); test('initial completed & completionDate', () { final Todo todo = Todo( completion: true, completionDate: DateTime(1970, 1, 1), description: 'Write some tests', ); expect(todo.completion, true); expect(todo.completionDate, DateTime(1970, 1, 1)); }); }); group('priority', () { test('no initial priority', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.priority, Priority.none); }); test('with initial priority', () { final Todo todo = Todo( priority: Priority.A, description: 'Write some tests', ); expect(todo.priority, Priority.A); }); }); group('creationDate', () { test('no initial creationDate', () { final DateTime now = DateTime.now(); final Todo todo = Todo( description: 'Write some tests', ); expect(todo.creationDate, DateTime(now.year, now.month, now.day)); }); test('with initial creationDate', () { final DateTime now = DateTime.now(); final Todo todo = Todo( creationDate: now, description: 'Write some tests', ); expect(todo.creationDate, DateTime(now.year, now.month, now.day)); }); }); group('description', () { test('no initial description', () { final Todo todo = Todo(); expect(todo.description, ''); }); test('with initial description', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.description, 'Write some tests'); }); }); group('projects', () { test('no initial projects', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.projects, []); }); test('with initial projects', () { final Todo todo = Todo( description: 'Write some tests +project1', ); expect(todo.projects, {'project1'}); }); }); group('contexts', () { test('no initial contexts', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.contexts, []); }); test('with initial contexts', () { final Todo todo = Todo( description: 'Write some tests @context1', ); expect(todo.contexts, {'context1'}); }); }); group('keyValues', () { test('no initial keyValues', () { final Todo todo = Todo( description: 'Write some tests', ); expect(todo.keyValues, []); }); test('with initial keyValues', () { final Todo todo = Todo( description: 'Write some tests key:value', ); expect(todo.keyValues, {'key:value'}); }); }); }); group('todo copyWith()', () { group('completion & completionDate', () { test('set completion', () { final DateTime now = DateTime.now(); final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(completion: true); expect(todo2.completion, true); expect(todo2.completionDate, DateTime(now.year, now.month, now.day)); }); test('set completion & completionDate', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith( completion: true, completionDate: DateTime(1970, 1, 1)); expect(todo2.completion, true); expect(todo2.completionDate, DateTime(1970, 1, 1)); }); test('unset completion', () { final Todo todo = Todo( completion: true, description: 'Write some tests', ); final todo2 = todo.copyWith(completion: false); expect(todo2.completion, false); expect(todo2.completionDate, null); }); }); group('priority', () { test('set priority', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(priority: Priority.A); expect(todo2.priority, Priority.A); }); test('unset priority', () { final Todo todo = Todo( priority: Priority.A, description: 'Write some tests', ); final todo2 = todo.copyWith(priority: Priority.none); expect(todo2.priority, Priority.none); }); }); group('creationDate', () {}); group('description', () { test('set description', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(description: 'Write more tests'); expect(todo2.description, 'Write more tests'); }); test('unset description', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(description: ''); expect(todo2.description, ''); }); }); group('projects', () { test('set projects', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(description: 'Write some tests +project2'); expect(todo2.projects, {'project2'}); }); test('unset projects', () { final Todo todo = Todo( description: 'Write some tests +project2', ); final todo2 = todo.copyWith(description: 'Write some tests'); expect(todo2.projects, []); }); }); group('contexts', () { test('set contexts', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(description: 'Write some tests @context2'); expect(todo2.contexts, {'context2'}); }); test('unset contexts', () { final Todo todo = Todo( description: 'Write some tests @context2', ); final todo2 = todo.copyWith(description: 'Write some tests'); expect(todo2.contexts, []); }); }); group('keyValues', () { test('set keyValues', () { final Todo todo = Todo( description: 'Write some tests', ); final todo2 = todo.copyWith(description: 'Write some tests key:value'); expect(todo2.keyValues, {'key:value'}); }); test('unset keyValues', () { final Todo todo = Todo( description: 'Write some tests key:value', ); final todo2 = todo.copyWith(description: 'Write some tests'); expect(todo2.keyValues, []); }); }); }); group('todo copyDiff()', () { test('copy explizit set attributes but keep creationDate if set', () { final DateTime now = DateTime.now(); final Todo todo = Todo( priority: Priority.A, description: 'Write some tests', ); final Todo todo2 = todo.copyDiff(completion: true); expect(todo2.completion, true); expect(todo2.completionDate, DateTime(now.year, now.month, now.day)); expect(todo2.creationDate, DateTime(now.year, now.month, now.day)); expect(todo2.priority, Priority.none); expect(todo2.description, ''); }); }); group('todo copyMerge()', () { test('do not overwrite attrs if not set in the diff', () { final DateTime now = DateTime.now(); Todo todo = Todo( completion: false, priority: Priority.A, creationDate: now, description: 'Write some tests +project1 @context1 key:value', ); final Todo diff = todo.copyDiff(completion: true); todo = todo.copyWith( priority: Priority.B, description: 'Write more tests +project1 @context1 key:value', ); final Todo todo2 = diff.copyMerge(todo); expect(todo2.priority, Priority.B); expect( todo2.description, 'Write more tests +project1 @context1 key:value'); expect(todo2.projects, {'project1'}); expect(todo2.contexts, {'context1'}); expect(todo2.keyValues, {'key:value'}); expect(todo2.completion, true); expect(todo2.completionDate, DateTime(now.year, now.month, now.day)); }); }); group('todo fromString()', () { group('todo completion', () { group('completed', () { test('short todo (RangeError)', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 Todo'); expect(todo.completion, true); expect(todo.completionDate, DateTime(2022, 11, 16)); }); test('simple todo', () { final Todo todo = Todo.fromString(value: 'x 2022-08-22 Write some tests'); expect(todo.completion, true); expect(todo.completionDate, DateTime(2022, 08, 22)); }); test('full todo', () { final Todo todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31'); expect(todo.completion, true); expect(todo.completionDate, DateTime(2022, 11, 16)); }); }); group('incompleted', () { test('short todo (RangeError)', () { final Todo todo = Todo.fromString(value: 'Todo'); expect(todo.completion, false); }); test('simple todo', () { final Todo todo = Todo.fromString(value: 'Write some tests'); expect(todo.completion, false); }); test('missing whitespace', () { final Todo todo = Todo.fromString(value: 'xWrite some tests'); expect(todo.completion, false); }); test('wrong mark', () { final Todo todo = Todo.fromString(value: 'X Write some tests'); expect(todo.completion, false); }); test('wrong position', () { final Todo todo = Todo.fromString(value: '(A) x Write some tests'); expect(todo.completion, false); }); }); group('edge cases', () { test('missing completion date', () { final DateTime now = DateTime.now(); final Todo todo = Todo.fromString(value: 'x Write some tests'); expect(todo.completion, true); expect(todo.completionDate, DateTime(now.year, now.month, now.day)); }); }); }); group('todo priority', () { group('with priority', () { test('incompleted short todo (RangeError)', () { final todo = Todo.fromString(value: '(A) Todo'); expect(todo.priority, Priority.A); }); test('incompleted simple todo (RangeError)', () { final todo = Todo.fromString(value: '(A) Write some tests'); expect(todo.priority, Priority.A); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-16 Write some tests +project @context due:2022-12-31', ); expect(todo.priority, Priority.A); }); test('incompleted todo with very low priority', () { final todo = Todo.fromString(value: '(Z) Todo'); expect(todo.priority, Priority.Z); }); test('completed short todo (RangeError)', () { final todo = Todo.fromString(value: 'x 2022-11-16 (A) Todo'); expect(todo.priority, Priority.A); }); test('completed simple todo', () { final todo = Todo.fromString(value: 'x 2022-11-16 (A) Write some tests'); expect(todo.priority, Priority.A); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.priority, Priority.A); }); test('completed todo with very low priority', () { final todo = Todo.fromString(value: 'x 2022-11-16 (Z) Todo'); expect(todo.priority, Priority.Z); }); }); group('without priority', () { test('incompleted short todo (RangeError)', () { final todo = Todo.fromString(value: 'Todo'); expect(todo.priority, Priority.none); }); test('incompleted simple todo', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.priority, Priority.none); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '2022-11-16 Write some tests +project @context due:2022-12-31'); expect(todo.priority, Priority.none); }); test('completed short todo (RangeError)', () { final todo = Todo.fromString(value: 'x 2022-11-16 Todo'); expect(todo.priority, Priority.none); }); test('completed simple todo', () { final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests'); expect(todo.priority, Priority.none); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.priority, Priority.none); }); test('missing parenthesis', () { final todo = Todo.fromString(value: 'A Write some tests'); expect(todo.priority, Priority.none); }); test('missing whitespace', () { final todo = Todo.fromString(value: '(A)Write some tests'); expect(todo.priority, Priority.none); }); test('wrong priority sign', () { final todo = Todo.fromString(value: '(a) Write some tests'); expect(todo.priority, Priority.none); }); test('wrong position', () { final todo = Todo.fromString(value: 'Write some tests (A)'); expect(todo.priority, Priority.none); }); }); }); group('todo creation date', () { group('with creation date', () { test('incompleted simple todo', () { final todo = Todo.fromString(value: '2022-11-01 Write some tests'); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); test('incompleted and with priority simple todo', () { final todo = Todo.fromString(value: '(A) 2022-11-01 Write some tests'); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); test('completed simple todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 2022-11-01 Write some tests'); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); test('completed and with priority simple todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests'); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.creationDate, DateTime.parse('2022-11-01')); }); }); }); group('todo completion date', () { group('with completion date', () { test('completed simple todo', () { final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests'); expect(todo.completionDate, DateTime.parse('2022-11-16')); }); test('completed and with priority simple todo', () { final todo = Todo.fromString(value: 'x 2022-11-16 (A) Write some tests'); expect(todo.completionDate, DateTime.parse('2022-11-16')); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.completionDate, DateTime.parse('2022-11-16')); }); }); group('without completion date', () { test('incompleted simple todo', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.completionDate, null); }); test('incompleted and with priority simple todo', () { final todo = Todo.fromString(value: '(A) Write some tests'); expect(todo.completionDate, null); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.completionDate, null); }); }); group('edge cases', () { test( 'incompleted with forbidden completiond date (is recognized as part of description)', () { final todo = Todo.fromString( value: '2022-11-16 2022-11-01 Write some tests', ); expect(todo.completionDate, null); expect(todo.creationDate, DateTime.parse('2022-11-16')); expect(todo.description, '2022-11-01 Write some tests'); }); test( 'incompleted with priority and forbidden completion date (is recognized as part of description)', () { final todo = Todo.fromString( value: '(A) 2022-11-16 2022-11-01 Write some tests', ); expect(todo.priority, Priority.A); expect(todo.completionDate, null); expect(todo.creationDate, DateTime.parse('2022-11-16')); expect(todo.description, '2022-11-01 Write some tests'); }); test('completed and missing completion date', () { final DateTime now = DateTime.now(); final Todo todo = Todo.fromString(value: 'x Write some tests'); expect(todo.completion, true); expect(todo.completionDate, DateTime(now.year, now.month, now.day)); }); test('completed with priority and missing completion date', () { final DateTime now = DateTime.now(); final Todo todo = Todo.fromString(value: 'x (A) Write some tests'); expect(todo.completion, true); expect(todo.completionDate, DateTime(now.year, now.month, now.day)); }); }); }); group('todo projects', () { test('no project tags', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.projects, []); }); test('single project tag', () { final todo = Todo.fromString(value: 'Write some tests +project'); expect(todo.projects, ['project']); }); test('multiple project tags', () { final todo = Todo.fromString(value: 'Write some tests +project1 +project2'); expect(todo.projects, ['project1', 'project2']); }); test('multiple project tags (not in sequence)', () { final todo = Todo.fromString(value: 'Write some +tests for +project'); expect(todo.projects, ['project', 'tests']); }); test('project tag with a special name', () { final todo = Todo.fromString(value: 'Write some tests +project_123+a-b-c'); expect(todo.projects, ['project_123+a-b-c']); }); test('project tag with uppercase characters', () { final todo = Todo.fromString(value: 'Write some tests +Project'); expect(todo.projects, ['Project']); }); test('project tag with project duplication', () { final todo = Todo.fromString(value: 'Write some tests +project +project'); expect(todo.projects, ['project']); }); test('similar project tags with uppercase and lowercase characters', () { final todo = Todo.fromString( value: 'Write some tests +project +project +Project'); expect(todo.projects, ['Project', 'project']); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.projects, ['project']); }); test('incompleted with priority full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.projects, ['project']); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.projects, ['project']); }); test('completed with priority full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.projects, ['project']); }); }); group('todo contexts', () { test('no context tag', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.contexts, []); }); test('single context tag', () { final todo = Todo.fromString(value: 'Write some @context'); expect(todo.contexts, ['context']); }); test('multiple context tags', () { final todo = Todo.fromString(value: 'Write some @context1 @context2'); expect(todo.contexts, ['context1', 'context2']); }); test('multiple context tags (not in sequence)', () { final todo = Todo.fromString(value: 'Write some @tests for @context'); expect(todo.contexts, ['context', 'tests']); }); test('context tag with a special name', () { final todo = Todo.fromString(value: 'Write some tests for @context_123+a-b-c'); expect(todo.contexts, ['context_123+a-b-c']); }); test('context tag with uppercase characters', () { final todo = Todo.fromString(value: 'Write some tests @Context'); expect(todo.contexts, ['Context']); }); test('context tag with context duplication', () { final todo = Todo.fromString(value: 'Write some tests @context @context'); expect(todo.contexts, ['context']); }); test('similar context tags with uppercase and lowercase characters', () { final todo = Todo.fromString( value: 'Write some tests +project @context @Context'); expect(todo.contexts, ['Context', 'context']); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.contexts, ['context']); }); test('incompleted with priority full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.contexts, ['context']); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.contexts, ['context']); }); test('completed with priority full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.contexts, ['context']); }); }); group('todo key values', () { test('no key value tag', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.keyValues, []); }); test('single key value tag', () { final todo = Todo.fromString(value: 'Write some tests key:value'); expect(todo.keyValues, {'key:value'}); }); test('single character key value tag', () { final todo = Todo.fromString(value: 'Write some tests k:v'); expect(todo.keyValues, {'k:v'}); }); test('multiple key value tags', () { final todo = Todo.fromString(value: 'Write some tests key1:value1 key2:value2'); expect(todo.keyValues, {'key1:value1', 'key2:value2'}); }); test('multiple key value tags (not in sequence)', () { final todo = Todo.fromString(value: 'Write some key1:value1 tests key2:value2'); expect(todo.keyValues, {'key1:value1', 'key2:value2'}); }); test('key value tag with a special name', () { final todo = Todo.fromString(value: 'Write some tests key-123:value_123'); expect(todo.keyValues, {'key-123:value_123'}); }); test('key value tag with uppercase characters', () { final todo = Todo.fromString(value: 'Write some tests Key:Value'); expect(todo.keyValues, {'Key:Value'}); }); test('key value tag with key value duplication', () { final todo = Todo.fromString(value: 'Write some tests key:value key:value'); expect(todo.keyValues, {'key:value'}); }); test('similar key value tags with uppercase and lowercase characters', () { final todo = Todo.fromString(value: 'Write some tests key:value Key:Value'); expect(todo.keyValues, {'Key:Value', 'key:value'}); }); test('invalid key value tag 1', () { final todo = Todo.fromString(value: 'Write some tests key1:value1:invalid'); expect(todo.keyValues, []); }); test('invalid key value tag 2', () { final todo = Todo.fromString(value: 'Write some tests key1::invalid'); expect(todo.keyValues, []); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.keyValues, {'due:2022-12-31'}); }); test('incompleted with priority full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.keyValues, {'due:2022-12-31'}); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.keyValues, {'due:2022-12-31'}); }); test('completed with priority full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.keyValues, {'due:2022-12-31'}); }); }); group('todo description', () { group('with description', () { test('incompleted with description', () { final todo = Todo.fromString(value: 'Write some tests'); expect(todo.description, 'Write some tests'); }); test('incompleted with description and priority', () { final todo = Todo.fromString(value: '(A) Write some tests'); expect(todo.description, 'Write some tests'); }); test('incompleted full todo', () { final todo = Todo.fromString( value: '(A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.description, 'Write some tests +project @context due:2022-12-31'); }); test('completed with description', () { final todo = Todo.fromString(value: 'x 2022-11-16 Write some tests'); expect(todo.description, 'Write some tests'); }); test('completed with description and priority', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) Write some tests', ); expect(todo.description, 'Write some tests'); }); test('completed full todo', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.description, 'Write some tests +project @context due:2022-12-31'); }); }); group('empty description', () { test('completed with projects', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 +project1 +project2'); expect(todo.description, '+project1 +project2'); }); test('completed with contexts', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 @context1 @context2'); expect(todo.description, '@context1 @context2'); }); test('completed with key-values', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 key1:val1 key2:val2'); expect(todo.description, 'key1:val1 key2:val2'); }); test('completed with all kind of tags', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 +project @context key:val'); expect(todo.description, '+project @context key:val'); }); test('completed', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16'); expect(todo.description, ''); }); test('completed with priority', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 (A)'); expect(todo.description, ''); }); test('completed with priority and creation date', () { final Todo todo = Todo.fromString(value: 'x 2022-11-16 (A) 2022-11-01'); expect(todo.description, ''); }); test('incompleted with projects', () { final Todo todo = Todo.fromString(value: '+project1 +project2'); expect(todo.description, '+project1 +project2'); }); test('incompleted with contexts', () { final Todo todo = Todo.fromString(value: '@context1 @context2'); expect(todo.description, '@context1 @context2'); }); test('incompleted with key-values', () { final Todo todo = Todo.fromString(value: 'key1:val1 key2:val2'); expect(todo.description, 'key1:val1 key2:val2'); }); test('incompleted with all kind of tags', () { final Todo todo = Todo.fromString(value: '+project @context key:val'); expect(todo.description, '+project @context key:val'); }); test('incompleted', () { final Todo todo = Todo.fromString(value: ''); expect(todo.description, ''); }); test('incompleted with priority', () { final Todo todo = Todo.fromString(value: '(A)'); expect(todo.description, ''); }); test('incompleted with priority and creation date', () { final Todo todo = Todo.fromString(value: '(A) 2022-11-01'); expect(todo.description, ''); }); }); }); }); group('todo dueDate', () { test('unset', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests', ); expect(todo.dueDate, null); }); test('set', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests due:2023-12-31', ); expect(todo.dueDate, DateTime(2023, 12, 31)); }); test('set but invalid', () { final todo = Todo.fromString( value: '2022-11-01 Write some tests due:yyyy-mm-dd', ); expect(todo.dueDate, null); }); }); group('todo toString()', () { test('full todo', () { const String value = 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31'; final todo = Todo.fromString( value: value, ); expect(todo.toString(), value); }); test('full todo with multiple whitespace', () { final todo = Todo.fromString( value: 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31', ); expect(todo.toString(), 'x 2022-11-16 (A) 2022-11-01 Write some tests +project @context due:2022-12-31'); }); }); } ================================================ FILE: test/todo/page/todo_create_edit_page_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/common/widget/chip.dart'; import 'package:ntodotxt/common/widget/contexts_dialog.dart'; import 'package:ntodotxt/common/widget/key_values_dialog.dart'; import 'package:ntodotxt/common/widget/priorities_dialog.dart'; import 'package:ntodotxt/common/widget/projects_dialog.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/page/todo_create_edit_page.dart'; class MaterialAppWrapper extends StatelessWidget { final Todo? initTodo; final Set projects; final Set contexts; final Set keyValues; const MaterialAppWrapper({ this.initTodo, this.projects = const {}, this.contexts = const {}, this.keyValues = const {}, super.key, }); @override Widget build(BuildContext context) { return MaterialApp( home: TodoCreateEditPage( initTodo: initTodo ?? Todo(), newTodo: initTodo == null ? true : false, projects: projects, contexts: contexts, keyValues: keyValues, ), ); } } void main() { group('TodoCreateEditPage', () { group('narrow view', () { group('create mode', () { testWidgets('found no SaveTodoIconButton if name is empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsNothing, ); }); testWidgets('found SaveTodoIconButton if name is not empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'Filter name'); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); }); testWidgets('found no DeleteTodoIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(DeleteTodoIconButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no TodoCompletionDateItem', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(TodoCompletionDateItem), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no DoneUndonePrimaryButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(DoneUndonePrimaryButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); group('edit mode', () { testWidgets('found no SaveTodoIconButton if todo has not be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveTodoIconButton if todo has be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoPriorityItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoPriorityItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoPriorityTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoPriorityTagDialog), matching: find.text('A'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoPriorityItem), matching: find.text('A'), ), findsOneWidget, ); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DeleteTodoIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(DeleteTodoIconButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found TodoCompletionDateItem', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(TodoCompletionDateItem), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DoneUndonePrimaryButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(DoneUndonePrimaryButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); }); group('wide view', () { group('create mode', () { testWidgets('found no SaveTodoIconButton if name is empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsNothing, ); }); testWidgets('found SaveTodoIconButton if name is not empty', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), 'Filter name'); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); }); testWidgets('found no DeleteTodoIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(DeleteTodoIconButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no TodoCompletionDateItem', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(TodoCompletionDateItem), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found no DoneUndonePrimaryButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); expect( find.byType(DoneUndonePrimaryButton), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); group('edit mode', () { testWidgets('found no SaveTodoIconButton if todo has not be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsNothing, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found SaveTodoIconButton if todo has be changed', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoPriorityItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoPriorityItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoPriorityTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoPriorityTagDialog), matching: find.text('A'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoPriorityItem), matching: find.text('A'), ), findsOneWidget, ); expect( find.descendant( of: find.byType(SaveTodoIconButton), matching: find.byType(IconButton), ), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DeleteTodoIconButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(DeleteTodoIconButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found TodoCompletionDateItem', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(TodoCompletionDateItem), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('found DoneUndonePrimaryButton', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 800); tester.view.devicePixelRatio = 1.0; await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); expect( find.byType(DoneUndonePrimaryButton), findsOneWidget, ); // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); }); group('default values', () { testWidgets('TodoPriorityItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoPriorityItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoPriorityItem), matching: find.text('none'), ), findsOneWidget, ); }); testWidgets('TodoProjectTagsItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.text('-'), ), findsOneWidget, ); }); testWidgets('TodoContextTagsItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoContextTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.text('-'), ), findsOneWidget, ); }); testWidgets('TodoKeyValueTagsItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.text('-'), ), findsOneWidget, ); }); testWidgets('TodoCreationDateItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCreationDateItem), find.byType(ListView), const Offset(0, -100), ); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoCreationDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoDueDateItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text('-'), ), findsOneWidget, ); }); }); group('non default values', () { testWidgets('TodoPriorityItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(priority: Priority.A, description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoPriorityItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoPriorityItem), matching: find.text(Priority.A.name), ), findsOneWidget, ); }); testWidgets('TodoProjectTagsItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something +project1 +project2'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project1', ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project2', ), ), findsOneWidget, ); }); testWidgets('TodoContextTagsItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something @context1 @context2'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoContextTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context1', ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context2', ), ), findsOneWidget, ); }); testWidgets('TodoKeyValueTagsItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something key1:val1 key2:val2'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'key1:val1', ), ), findsOneWidget, ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'key2:val2', ), ), findsOneWidget, ); }); testWidgets('TodoCompletionDateItem (incompleted)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCompletionDateItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoCompletionDateItem), matching: find.text('-'), ), findsOneWidget, ); }); testWidgets('TodoCompletionDateItem (completed)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(completion: true, description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCompletionDateItem), find.byType(ListView), const Offset(0, -100), ); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoCompletionDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoCreationDateItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCreationDateItem), find.byType(ListView), const Offset(0, -100), ); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoCreationDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoDueDateItem', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something due:2024-02-27'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text('2024-02-27'), ), findsOneWidget, ); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'due:2024-02-27', ), ), findsOneWidget, ); }); }); group('update values', () { testWidgets('TodoDescriptionTextField (project)', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDescriptionTextField), find.byType(ListView), const Offset(0, -100), ); await tester.enterText( find.descendant( of: find.byType(TodoDescriptionTextField), matching: find.byType(TextFormField), ), 'Code something +project1', ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project1', ), ), findsOneWidget, ); }); testWidgets('TodoDescriptionTextField (context)', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDescriptionTextField), find.byType(ListView), const Offset(0, -100), ); await tester.enterText( find.descendant( of: find.byType(TodoDescriptionTextField), matching: find.byType(TextFormField), ), 'Code something @context1', ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoContextTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context1', ), ), findsOneWidget, ); }); testWidgets('TodoDescriptionTextField (key value)', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDescriptionTextField), find.byType(ListView), const Offset(0, -100), ); await tester.enterText( find.descendant( of: find.byType(TodoDescriptionTextField), matching: find.byType(TextFormField), ), 'Code something key1:val1', ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'key1:val1', ), ), findsOneWidget, ); }); testWidgets('TodoDescriptionTextField (due date)', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDescriptionTextField), find.byType(ListView), const Offset(0, -100), ); await tester.enterText( find.descendant( of: find.byType(TodoDescriptionTextField), matching: find.byType(TextFormField), ), 'Code something due:2024-02-27', ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text('2024-02-27'), ), findsOneWidget, ); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -100), ); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'due:2024-02-27', ), ), findsOneWidget, ); }); testWidgets('TodoPriorityItem', (tester) async { await tester.pumpWidget(const MaterialAppWrapper()); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoPriorityItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoPriorityItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoPriorityTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoPriorityTagDialog), matching: find.text('A'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoPriorityItem), matching: find.text('A'), ), findsOneWidget, ); }); testWidgets('TodoProjectTagsItem by tap chip', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( projects: {'project1', 'project2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoProjectTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoProjectTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoProjectTagDialog), matching: find.text('project1'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project1', ), ), findsOneWidget, ); }); testWidgets('TodoProjectTagsItem by enter in textfield', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( projects: {'project1', 'project2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoProjectTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoProjectTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoProjectTagDialog)); await tester.pumpAndSettle(); await tester.enterText( find.descendant( of: find.byType(TodoProjectTagDialog), matching: find.byType(TextFormField), ), 'project3', ); await tester.tap( find.descendant( of: find.byType(TodoProjectTagDialog), matching: find.text('Add'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoProjectTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'project3', ), ), findsOneWidget, ); }); testWidgets('TodoContextTagsItem by tap chip', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( contexts: {'context1', 'context2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoContextTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoContextTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoContextTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoContextTagDialog), matching: find.text('context1'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context1', ), ), findsOneWidget, ); }); testWidgets('TodoContextTagsItem by enter in textfield', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( contexts: {'context1', 'context2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoContextTagsItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoContextTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoContextTagDialog)); await tester.pumpAndSettle(); await tester.enterText( find.descendant( of: find.byType(TodoContextTagDialog), matching: find.byType(TextFormField), ), 'context3', ); await tester.tap( find.descendant( of: find.byType(TodoContextTagDialog), matching: find.text('Add'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoContextTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'context3', ), ), findsOneWidget, ); }); testWidgets('TodoKeyValueTagsItem by tap chip', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( keyValues: {'key1:val1', 'key2:val2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -200), ); await tester.tap(find.byType(TodoKeyValueTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoKeyValueTagDialog)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(TodoKeyValueTagDialog), matching: find.text('key1:val1'), ), ); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'key1:val1', ), ), findsOneWidget, ); }); testWidgets('TodoKeyValueTagsItem by enter in textfield', (tester) async { await tester.pumpWidget( const MaterialAppWrapper( keyValues: {'key1:val1', 'key2:val2'}, ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoKeyValueTagsItem), find.byType(ListView), const Offset(0, -200), ); await tester.tap(find.byType(TodoKeyValueTagsItem)); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(TodoKeyValueTagDialog)); await tester.pumpAndSettle(); await tester.enterText( find.descendant( of: find.byType(TodoKeyValueTagDialog), matching: find.byType(TextFormField), ), 'key3:val3', ); await tester.tap( find.descendant( of: find.byType(TodoKeyValueTagDialog), matching: find.text('Add'), ), ); await tester.pumpAndSettle(); await tester.drag(find.byType(DraggableScrollableSheet), const Offset(0, 500)); // Dismiss dialog. await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoKeyValueTagsItem), matching: find.byWidgetPredicate( (Widget widget) => widget is BasicChip && widget.label == 'key3:val3', ), ), findsOneWidget, ); }); testWidgets('TodoCompletionDateItem (set)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo( completion: false, description: 'Code something', ), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCompletionDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(DoneUndonePrimaryButton)); await tester.pumpAndSettle(); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoCompletionDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoCompletionDateItem (already set)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'x 2024-01-01 Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCompletionDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoCompletionDateItem)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(DatePickerDialog), matching: find.text('OK'), ), ); await tester.pumpAndSettle(); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoCompletionDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoCompletionDateItem (unset)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(completion: true, description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoCompletionDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(DoneUndonePrimaryButton)); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoCompletionDateItem), matching: find.text('-'), ), findsOneWidget, ); }); testWidgets('TodoDueDateItem (set)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoDueDateItem)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(DatePickerDialog), matching: find.text('OK'), ), ); await tester.pumpAndSettle(); final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoDueDateItem (already set)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something due:2024-02-27'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap(find.byType(TodoDueDateItem)); await tester.pumpAndSettle(); await tester.tap( find.descendant( of: find.byType(DatePickerDialog), matching: find.text('OK'), ), ); await tester.pumpAndSettle(); final DateTime initial = DateTime.parse('2024-02-27'); final String today = '${initial.year.toString()}-${initial.month.toString().padLeft(2, '0')}-${initial.day.toString().padLeft(2, '0')}'; expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text(today), ), findsOneWidget, ); }); testWidgets('TodoDueDateItem (unset)', (tester) async { await tester.pumpWidget( MaterialAppWrapper( initTodo: Todo(description: 'Code something due:2024-02-27'), ), ); await tester.pumpAndSettle(); await tester.dragUntilVisible( find.byType(TodoDueDateItem), find.byType(ListView), const Offset(0, -100), ); await tester.tap( find.descendant( of: find.byType(TodoDueDateItem), matching: find.byType(IconButton), ), ); await tester.pumpAndSettle(); expect( find.descendant( of: find.byType(TodoDueDateItem), matching: find.text('-'), ), findsOneWidget, ); }); }); }); } ================================================ FILE: test/todo/page/todo_list_page_test.dart ================================================ import 'dart:io'; import 'package:file/memory.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/database/controller/database.dart'; import 'package:ntodotxt/filter/controller/filter_controller.dart'; import 'package:ntodotxt/filter/model/filter_model.dart' show Filter, ListFilter, ListGroup, ListOrder; import 'package:ntodotxt/filter/repository/filter_repository.dart'; import 'package:ntodotxt/filter/state/filter_cubit.dart'; import 'package:ntodotxt/filter/state/filter_list_bloc.dart'; import 'package:ntodotxt/filter/state/filter_list_event.dart'; import 'package:ntodotxt/setting/controller/setting_controller.dart' show SettingController; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/setting/state/interaction_settings_cubit.dart'; import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/todo/page/todo_list_page.dart'; import 'package:ntodotxt/todo/repository/todo_list_repository.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class TodoListPageMaterialApp extends StatelessWidget { final DatabaseController dbController = const DatabaseController(inMemoryDatabasePath); final File localFile; final Filter? filter; const TodoListPageMaterialApp({ required this.localFile, this.filter, super.key, }); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (BuildContext context) => SettingRepository(SettingController(dbController)), ), RepositoryProvider( create: (BuildContext context) { return TodoListRepository( LocalTodoListApi.fromFile(localFile: localFile)); }, ), RepositoryProvider( create: (BuildContext context) => FilterRepository( FilterController(dbController), ), ), BlocProvider( create: (BuildContext context) => InteractionSettingsCubit( repository: context.read(), ), ), ], child: Builder( builder: (BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => FilterCubit( settingRepository: context.read(), filterRepository: context.read(), filter: filter ?? const Filter(), ), ), BlocProvider( create: (BuildContext context) => TodoListBloc( repository: context.read(), ) ..add(const TodoListSubscriptionRequested()) ..add(const TodoListSynchronizationRequested()), ), BlocProvider( create: (BuildContext context) { return FilterListBloc( repository: context.read(), )..add(const FilterListSubscriped()); }, ), ], child: MaterialApp( home: TodoListPage(filter: filter ?? const Filter()), ), ); }, ), ); } } void main() { final MemoryFileSystem fs = MemoryFileSystem(); late File file; setUp(() async {}); group('Order', () { setUp(() async { file = fs.file('todoOrder.txt'); await file.create(); await file.writeAsString( [ '2023-12-02 TODOC', '2023-12-02 todoA', '2023-12-02 TodoB', ].join('\n'), flush: true, ); }); testWidgets('default', (tester) async { final List expectedTiles = [ 'todoA', 'TodoB', 'TODOC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('ascending', (tester) async { final List expectedTiles = [ 'todoA', 'TodoB', 'TODOC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('descending', (tester) async { final List expectedTiles = [ 'TODOC', 'TodoB', 'todoA', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); group('Filter', () { setUp(() async { file = fs.file('todoFilter.txt'); await file.create(); await file.writeAsString( [ 'x 2023-12-04 2023-12-02 TodoC', '2023-12-02 TodoA', 'x 2023-12-03 2023-12-02 TodoB', ].join('\n'), flush: true, ); }); testWidgets('all', (tester) async { final List expectedTiles = [ 'TodoA', 'TodoB', 'TodoC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('completed only', (tester) async { final List expectedTiles = [ 'TodoB', 'TodoC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.completedOnly, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('incompleted only', (tester) async { final List expectedTiles = [ 'TodoA', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.incompletedOnly, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable todoTiles = tester.widgetList(find.byType(TodoListTile)); expect(todoTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(todoTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); group('Group by', () { group('none', () { setUp(() async { file = fs.file('todoGroupByNone.txt'); await file.create(); await file.writeAsString( [ 'x 2023-13-04 2023-12-02 TodoB', '2023-12-02 TodoC', '(B) 2023-12-02 TodoA', ].join('\n'), flush: true, ); }); testWidgets('ascending', (tester) async { final List expectedTiles = [ 'All', 'TodoA', 'TodoC', 'TodoB', // Completed todo come always at last. ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('descending', (tester) async { final List expectedTiles = [ 'All', 'TodoC', 'TodoA', 'TodoB', // Completed todo come always at last. ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.none, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); group('upcoming', () { setUp(() async { final DateTime now = DateTime.now(); final String today = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; final String tomorrow = '${now.year.toString()}-${now.month.toString().padLeft(2, '0')}-${(now.day + 1).toString().padLeft(2, '0')}'; file = fs.file('todoGroupByUpcoming.txt'); await file.create(); await file.writeAsString( [ '2023-12-02 TodoA1', 'x 2023-12-04 2023-12-02 TodoB2 due:1970-01-01', '2023-12-02 TodoB1 due:1970-01-01', '2023-12-02 TodoC2 due:$today', '2023-12-02 TodoC1 due:$today', '2023-12-02 TodoD2 due:$tomorrow', '(B) 2023-12-02 TodoD1 due:$tomorrow', '2023-12-02 TodoA2', ].join('\n'), flush: true, ); }); testWidgets('ascending', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 1600); tester.view.devicePixelRatio = 1.0; final List expectedTiles = [ 'Deadline passed', 'TodoB1', 'TodoB2', // Completed todo come always at last. 'Today', 'TodoC1', 'TodoC2', 'Upcoming', 'TodoD1', 'TodoD2', 'No deadline', 'TodoA1', 'TodoA2', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.upcoming, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); testWidgets('descending', (tester) async { // Increase size to ensure all elements in list are visible. tester.view.physicalSize = const Size(800, 1600); tester.view.devicePixelRatio = 1.0; final List expectedTiles = [ 'Deadline passed', 'TodoB1', 'TodoB2', // Completed todo come always at last. 'Today', 'TodoC2', 'TodoC1', 'Upcoming', 'TodoD2', 'TodoD1', 'No deadline', 'TodoA2', 'TodoA1', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.upcoming, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } // resets the screen to its original size after the test end addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); }); }); group('priority', () { setUp(() async { file = fs.file('todoGroupByPriority.txt'); await file.create(); await file.writeAsString( [ '2023-12-02 TodoD', 'x 2023-12-04 (B) 2023-12-02 TodoB2', '(B) 2023-12-02 TodoB1', 'x 2023-12-04 (A) 2023-12-02 TodoA1', '(A) 2023-12-02 TodoA2', '2023-12-02 TodoC', ].join('\n'), flush: true, ); }); testWidgets('ascending', (tester) async { final List expectedTiles = [ 'A', 'TodoA2', 'TodoA1', // Completed todo come always at last. 'B', 'TodoB1', 'TodoB2', // Completed todo come always at last. 'No priority', 'TodoC', 'TodoD', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.priority, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('descending', (tester) async { final List expectedTiles = [ 'No priority', 'TodoD', 'TodoC', 'B', 'TodoB1', 'TodoB2', // Completed todo come always at last. 'A', 'TodoA2', 'TodoA1', // Completed todo come always at last. ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.priority, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); group('project', () { setUp(() async { file = fs.file('todoGroupByProject.txt'); await file.create(); await file.writeAsString( [ '2023-12-02 TodoD', '(B) 2023-12-02 TodoB2 +project2', '2023-12-02 TodoB1 +project1', 'x 2023-13-04 2023-12-02 TodoA1 +project1', '2023-12-02 TodoA2 +project2', '2023-12-02 TodoC', ].join('\n'), flush: true, ); }); testWidgets('ascending', (tester) async { final List expectedTiles = [ 'project1', 'TodoB1 +project1', 'TodoA1 +project1', // Completed todo come always at last. 'project2', 'TodoA2 +project2', 'TodoB2 +project2', 'No project', 'TodoC', 'TodoD', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.project, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('descending', (tester) async { final List expectedTiles = [ 'project2', 'TodoB2 +project2', 'TodoA2 +project2', 'project1', 'TodoB1 +project1', 'TodoA1 +project1', // Completed todo come always at last. 'No project', 'TodoD', 'TodoC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.project, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); group('context', () { setUp(() async { file = fs.file('todoGroupByContext.txt'); await file.create(); await file.writeAsString( [ '2023-12-02 TodoD', '(B) 2023-12-02 TodoB2 @context2', '2023-12-02 TodoB1 @context1', 'x 2023-12-04 2023-12-02 TodoA1 @context1', '2023-12-02 TodoA2 @context2', '2023-12-02 TodoC', ].join('\n'), flush: true, ); }); testWidgets('ascending', (tester) async { final List expectedTiles = [ 'context1', 'TodoB1 @context1', 'TodoA1 @context1', // Completed todo come always at last. 'context2', 'TodoA2 @context2', 'TodoB2 @context2', 'No context', 'TodoC', 'TodoD', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.ascending, filter: ListFilter.all, group: ListGroup.context, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); testWidgets('descending', (tester) async { final List expectedTiles = [ 'context2', 'TodoB2 @context2', 'TodoA2 @context2', 'context1', 'TodoB1 @context1', 'TodoA1 @context1', // Completed todo come always at last. 'No context', 'TodoD', 'TodoC', ]; await tester.pumpWidget(TodoListPageMaterialApp( localFile: file, filter: const Filter( order: ListOrder.descending, filter: ListFilter.all, group: ListGroup.context, ), )); await tester.pumpAndSettle(); Iterable listTiles = tester.widgetList(find.byType(ListTile)); expect(listTiles.length, expectedTiles.length); for (int i = 0; i < expectedTiles.length; i++) { Finder element = find.descendant( of: find.byWidget(listTiles.elementAt(i)), matching: find.text(expectedTiles[i], findRichText: true), ); await tester.ensureVisible(element); expect(element, findsOneWidget); } }); }); }); } ================================================ FILE: test/todo/state/todo_cubit_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/state/todo_cubit.dart'; import 'package:ntodotxt/todo/state/todo_state.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Todo todo; setUp(() async {}); group('initial', () { test('initial state', () { todo = Todo(description: 'Write some tests'); final TodoCubit todoBloc = TodoCubit(todo: todo); expect( todoBloc.state, TodoSuccess(todo: todo), ); }); }); group('completion', () { test('set', () async { todo = Todo( completion: false, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion(completion: true); expect( bloc.state, TodoSuccess( todo: Todo( completion: true, completionDate: DateTime.now(), description: todo.description, ), ), ); }); test('set with completionDate', () async { todo = Todo( completion: false, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion( completion: true, completionDate: DateTime(2025, 1, 1), ); expect( bloc.state, TodoSuccess( todo: Todo( completion: true, completionDate: DateTime(2025, 1, 1), description: todo.description, ), ), ); }); test('set with completionDate and incorrectly set completion attribute', () async { todo = Todo( completion: false, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion( completion: false, completionDate: DateTime(2025, 1, 1), ); expect( bloc.state, TodoSuccess( todo: Todo( completion: true, completionDate: DateTime(2025, 1, 1), description: todo.description, ), ), ); }); test( 'set with completionDate and without explicitly set completion attribute', () async { todo = Todo( completion: false, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion(completionDate: DateTime(2025, 1, 1)); expect( bloc.state, TodoSuccess( todo: Todo( completion: true, completionDate: DateTime(2025, 1, 1), description: todo.description, ), ), ); }); test('unset', () async { todo = Todo( completion: true, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion(completion: false); expect( bloc.state, TodoSuccess( todo: Todo( completion: false, completionDate: null, description: todo.description, ), ), ); }); test('toggle true', () async { todo = Todo( completion: false, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion(); expect( bloc.state, TodoSuccess( todo: Todo( completion: true, completionDate: DateTime.now(), description: todo.description, ), ), ); }); test('toggle false', () async { todo = Todo( completion: true, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.toggleCompletion(); expect( bloc.state, TodoSuccess( todo: Todo( completion: false, completionDate: null, description: todo.description, ), ), ); }); }); group('description', () { test('set', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateDescription('Write more tests'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write more tests', ), ), ); }); }); group('priority', () { test('set', () async { todo = Todo( priority: Priority.none, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.setPriority(Priority.A); expect( bloc.state, TodoSuccess( todo: Todo( priority: Priority.A, description: todo.description, ), ), ); }); test('update', () async { todo = Todo( priority: Priority.A, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.setPriority(Priority.B); expect( bloc.state, TodoSuccess( todo: Todo( priority: Priority.B, description: todo.description, ), ), ); }); test('unset', () async { todo = Todo( priority: Priority.A, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.unsetPriority(); expect( bloc.state, TodoSuccess( todo: Todo( priority: Priority.none, description: todo.description, ), ), ); }); test('unset (already unset)', () async { todo = Todo( priority: Priority.none, description: 'Write some tests', ); final TodoCubit bloc = TodoCubit(todo: todo); bloc.unsetPriority(); expect( bloc.state, TodoSuccess( todo: Todo( priority: Priority.none, description: todo.description, ), ), ); }); }); group('projects', () { group('add', () { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addProject('project1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1', ), ), ); }); test('additional', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addProject('project2'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1 +project2', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addProject('project 2'); expect( bloc.state, TodoError( message: 'Invalid project tag: project 2', todo: Todo( description: 'Write some tests', ), ), ); }); test('duplication', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addProject('project1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1', ), ), ); }); test('no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addProject('Project1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1 +Project1', ), ), ); }); }); group('update', () { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateProjects({'project1', 'project2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1 +project2', ), ), ); }); test('multiple entries (add & remove)', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateProjects({'project2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project2', ), ), ); }); test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateProjects({'project1', 'project 2'}); expect( bloc.state, TodoError( message: 'Invalid project tag: project 2', todo: Todo( description: 'Write some tests', ), ), ); }); test('multiple entries / duplication', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateProjects({'project1', 'project2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1 +project2', ), ), ); }); test('multiple entries / no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateProjects({'project1', 'Project1', 'Project2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests +project1 +Project1 +Project2', ), ), ); }); }); group('remove', () { test('initial', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeProject('project1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests +project1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeProject('project 1'); expect( bloc.state, TodoError( message: 'Invalid project tag: project 1', todo: Todo( description: 'Write some tests +project1', ), ), ); }); test('not exists', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeProject('project1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); }); }); group('contexts', () { group('add', () { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addContext('context1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1', ), ), ); }); test('additional', () async { todo = Todo(description: 'Write some tests @context1 @context2'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addContext('context2'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1 @context2', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addContext('context 2'); expect( bloc.state, TodoError( message: 'Invalid context tag: context 2', todo: Todo( description: 'Write some tests', ), ), ); }); test('duplication', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addContext('context1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1', ), ), ); }); test('no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addContext('Context1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1 @Context1', ), ), ); }); }); group('update', () { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateContexts({'context1', 'context2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1 @context2', ), ), ); }); test('multiple entries (add & remove)', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateContexts({'context2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context2', ), ), ); }); test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateContexts({'context1', 'context 2'}); expect( bloc.state, TodoError( message: 'Invalid context tag: context 2', todo: Todo( description: 'Write some tests', ), ), ); }); test('multiple entries / duplication', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateContexts({'context1', 'context2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1 @context2', ), ), ); }); test('multiple entries / no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateContexts({'context1', 'Context1', 'Context2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests @context1 @Context1 @Context2', ), ), ); }); }); group('remove', () { test('initial', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeContext('context1'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests @context1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeContext('context 1'); expect( bloc.state, TodoError( message: 'Invalid context tag: context 1', todo: Todo( description: 'Write some tests @context1', ), ), ); }); test('not exists', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeContext('context2'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); }); }); group('key values', () { group('add', () { test('initial', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('key:val'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests key:val', ), ), ); }); test('additional', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('key:val'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:bar key:val', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('key_val'); expect( bloc.state, TodoError( message: 'Invalid key value tag: key_val', todo: Todo( description: 'Write some tests', ), ), ); }); test('duplication', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('foo:bar'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:bar', ), ), ); }); test('no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('Foo:bar'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:bar Foo:bar', ), ), ); }); test('duplication / update value', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.addKeyValue('foo:new'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:new', ), ), ); }); }); group('update', () { test('multiple entries', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'key1:val1', 'key2:val2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests key1:val1 key2:val2', ), ), ); }); test('multiple entries (add & remove)', () async { todo = Todo(description: 'Write some tests key1:val1'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'key2:val2'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests key2:val2', ), ), ); }); test('multiple entries / invalid format', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'key1:val1', 'key2_val2'}); expect( bloc.state, TodoError( message: 'Invalid key value tag: key2_val2', todo: Todo( description: 'Write some tests', ), ), ); }); test('multiple entries / duplication', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'key1:val1', 'foo:bar'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:bar key1:val1', ), ), ); }); test('multiple entries / no duplication / case insensitive', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'foo:bar', 'Key1:val1', 'Foo:bar'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:bar Key1:val1 Foo:bar', ), ), ); }); test('multiple entries / update value', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.updateKeyValues({'key1:val1', 'foo:new'}); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests foo:new key1:val1', ), ), ); }); }); group('remove', () { test('add', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeKeyValue('foo:bar'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); test('invalid format', () async { todo = Todo(description: 'Write some tests foo:bar'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeKeyValue('key_val'); expect( bloc.state, TodoError( message: 'Invalid key value tag: key_val', todo: Todo( description: 'Write some tests foo:bar', ), ), ); }); test('not exits', () async { todo = Todo(description: 'Write some tests'); final TodoCubit bloc = TodoCubit(todo: todo); bloc.removeKeyValue('key:val'); expect( bloc.state, TodoSuccess( todo: Todo( description: 'Write some tests', ), ), ); }); }); }); } ================================================ FILE: test/todo/state/todo_list_bloc_test.dart ================================================ import 'dart:io'; import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/todo/api/todo_list_api.dart'; import 'package:ntodotxt/todo/model/todo_model.dart'; import 'package:ntodotxt/todo/repository/todo_list_repository.dart'; import 'package:ntodotxt/todo/state/todo_list_bloc.dart'; import 'package:ntodotxt/todo/state/todo_list_event.dart'; import 'package:ntodotxt/todo/state/todo_list_state.dart'; File mockTodoListFile(List rawTodoList) { final MemoryFileSystem fs = MemoryFileSystem(); final File file = fs.file('todo.txt'); file.createSync(); file.writeAsStringSync( rawTodoList.join(Platform.lineTerminator), flush: true, ); return file; } TodoListRepository mockLocalTodoListRepository(File todoFile) { final LocalTodoListApi api = LocalTodoListApi.fromFile(localFile: todoFile); final TodoListRepository repository = TodoListRepository(api); return repository; } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late File todoFile; late TodoListRepository repository; late Todo todo; setUp(() { todoFile = mockTodoListFile([]); repository = mockLocalTodoListRepository(todoFile); }); group('Initial', () { test('initial state', () { final TodoListBloc todoListBloc = TodoListBloc(repository: repository); expect(todoListBloc.state is TodoListLoading, true); expect(todoListBloc.state.todoList, []); }); }); group('TodoListSynchronizationRequested', () { test('initial state', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add(const TodoListSynchronizationRequested()); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( const TodoListSuccess(todoList: []), ), ], ), ); }); }); group('TodoListTodoSubmitted', () { group('completion', () { setUp(() async { todo = Todo( id: '1', completion: false, description: 'Write some tests', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(completion: true), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(completion: true)], ), ), ], ), ); }); }); group('priority', () { setUp(() async { todo = Todo( id: '1', priority: Priority.A, description: 'Write some tests', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(priority: Priority.B), ), ); await expectLater( bloc.stream, emitsThrough( TodoListSuccess( todoList: [todo.copyWith(priority: Priority.B)], ), ), ); }); test('unset', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(priority: Priority.none), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(priority: Priority.none)], ), ), ], ), ); }); }); group('description', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(description: 'Write more tests'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(description: 'Write more tests')], ), ), ], ), ); }); }); group('projects', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests +project1', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith( description: 'Write some tests +project1 +project2'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests +project1 +project2', ) ], ), ), ], ), ); }); test('unset', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(description: 'Write some tests'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests', ) ], ), ), ], ), ); }); }); group('contexts', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests @context1', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith( description: 'Write some tests @context1 @context2'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests @context1 @context2', ) ], ), ), ], ), ); }); test('unset', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(description: 'Write some tests'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests', ) ], ), ), ], ), ); }); }); group('key values', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests foo:bar', ); }); test('set', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith( description: 'Write some tests foo:bar key:val'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests foo:bar key:val', ) ], ), ), ], ), ); }); test('unset', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoSubmitted( todo: todo.copyWith(description: 'Write some tests'), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo.copyWith( description: 'Write some tests', ) ], ), ), ], ), ); }); }); }); group('TodoListTodoDeleted', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests', ); }); test('delete existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoDeleted(todo: todo), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( const TodoListSuccess(todoList: []), ), ], ), ); }); test('delete non-existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoDeleted( todo: Todo( id: '2', description: 'Write some tests!!!', ), ), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [ todo, ], ), ), ], ), ); }); }); group('TodoListTodoCompletionToggled', () { group('toggle to completed', () { setUp(() async { todo = Todo( id: '1', description: 'Write some tests', ); }); test('toggle existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoCompletionToggled(todo: todo, completion: true), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(completion: true)], ), ), ], ), ); }); test('toggle non-existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoCompletionToggled(todo: todo, completion: true), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(completion: true)], ), ), ], ), ); }); }); group('toggle to incompleted', () { setUp(() async { todo = Todo( id: '1', completion: true, description: 'Write some tests', ); }); test('toggle existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoSubmitted( todo: todo, ), ) ..add( TodoListTodoCompletionToggled(todo: todo, completion: false), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(completion: false)], ), ), ], ), ); }); test('toggle non-existing', () async { final TodoListBloc bloc = TodoListBloc(repository: repository); bloc ..add(const TodoListSubscriptionRequested()) ..add( TodoListTodoCompletionToggled(todo: todo, completion: false), ); await expectLater( bloc.stream, emitsInOrder( [ emitsThrough( TodoListSuccess( todoList: [todo.copyWith(completion: false)], ), ), ], ), ); }); }); }); } ================================================ FILE: test/todo_file/state/todo_file_cubit_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/setting/controller/fake_setting_controller.dart'; import 'package:ntodotxt/setting/repository/setting_repository.dart' show SettingRepository; import 'package:ntodotxt/setting/repository/setting_repository.dart'; import 'package:ntodotxt/todo_file/state/todo_file_cubit.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; void main() { group('TodoFileCubit', () { test('Default values', () { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); expect(cubit.state, isA()); expect(cubit.state.todoFilename, 'todo.txt'); expect(cubit.state.doneFilename, 'done.txt'); expect(cubit.state.localPath, '/'); expect(cubit.state.remotePath, '/'); }); }); group('saveLocalPath', () { test('Null value', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveLocalPath(null); expect(cubit.state.localPath, '/'); }); test('Without trailing /', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveLocalPath('/local'); expect(cubit.state.localPath, '/local/'); }); test('With trailing /', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveLocalPath('/local/'); expect(cubit.state.localPath, '/local/'); }); }); group('saveRemotePath', () { test('Null value', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveRemotePath(null); expect(cubit.state.remotePath, '/'); }); test('Without trailing /', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveRemotePath('/remote'); expect(cubit.state.remotePath, '/remote/'); }); test('With trailing /', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveRemotePath('/remote/'); expect(cubit.state.remotePath, '/remote/'); }); }); group('saveLocalFilename', () { test('Null value', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveLocalFilename(null); expect(cubit.state.todoFilename, 'todo.txt'); }); test('With value', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), ); await cubit.saveLocalFilename('todo2.txt'); expect(cubit.state.todoFilename, 'todo2.txt'); }); }); group('resetToDefaults', () { test('Reset to defaults', () async { final TodoFileCubit cubit = TodoFileCubit( repository: SettingRepository(FakeSettingController()), todoFilename: 'todo2.txt', doneFilename: 'done2.txt', localPath: '/local', remotePath: '/remote', ); await cubit.resetToDefaults(); expect(cubit.state, isA()); expect(cubit.state.todoFilename, 'todo.txt'); expect(cubit.state.doneFilename, 'done.txt'); expect(cubit.state.localPath, '/'); expect(cubit.state.remotePath, '/'); }); }); group('resetTodoFileSettings', () {}); } ================================================ FILE: test/todo_file/state/todo_file_state_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/todo_file/state/todo_file_state.dart'; void main() { group('TodoFileLoading', () { test('localPath with trailing /', () { TodoFileLoading state = TodoFileLoading(localPath: 'path/'); expect(state.localPath, 'path/'); }); test('localPath without trailing /', () { TodoFileLoading state = TodoFileLoading(localPath: 'path'); expect(state.localPath, 'path/'); }); test('remotePath with trailing /', () { TodoFileLoading state = TodoFileLoading(remotePath: 'path/'); expect(state.remotePath, 'path/'); }); test('remotePath without trailing /', () { TodoFileLoading state = TodoFileLoading(remotePath: 'path'); expect(state.remotePath, 'path/'); }); test('copyWith() with todoFilename', () { TodoFileLoading state = TodoFileLoading( todoFilename: 'todo.txt', ); TodoFileLoading state2 = state.copyWith(todoFilename: 'todo2.txt'); expect(state2.todoFilename, 'todo2.txt'); }); test('copyWith() with doneFilename', () { TodoFileLoading state = TodoFileLoading( doneFilename: 'done.txt', ); TodoFileLoading state2 = state.copyWith(doneFilename: 'done2.txt'); expect(state2.doneFilename, 'done2.txt'); }); test('copyWith() with localPath', () { TodoFileLoading state = TodoFileLoading( localPath: '/local', ); TodoFileLoading state2 = state.copyWith(localPath: '/local2/'); expect(state2.localPath, '/local2/'); }); test('copyWith() with remotePath', () { TodoFileLoading state = TodoFileLoading( remotePath: '/remote', ); TodoFileLoading state2 = state.copyWith(remotePath: '/remote2/'); expect(state2.remotePath, '/remote2/'); }); test('toString() without trailing /', () { TodoFileLoading state = TodoFileLoading( todoFilename: 'todo.txt', localPath: '/local', remotePath: '/remote', ); expect('$state', 'TodoFileLoading { localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('toString() with trailing /', () { TodoFileLoading state = TodoFileLoading( todoFilename: 'todo.txt', localPath: '/local/', remotePath: '/remote/', ); expect('$state', 'TodoFileLoading { localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('load()', () { TodoFileLoading state = TodoFileLoading(); TodoFileLoading state2 = state.load(); expect(state2, isA()); }); test('ready()', () { TodoFileLoading state = TodoFileLoading(); TodoFileReady state2 = state.ready(); expect(state2, isA()); }); test('error()', () { TodoFileLoading state = TodoFileLoading(); TodoFileError state2 = state.error(message: 'error'); expect(state2, isA()); }); }); group('TodoFileReady', () { test('localPath with trailing /', () { TodoFileReady state = TodoFileReady(localPath: 'path/'); expect(state.localPath, 'path/'); }); test('localPath without trailing /', () { TodoFileReady state = TodoFileReady(localPath: 'path'); expect(state.localPath, 'path/'); }); test('remotePath with trailing /', () { TodoFileReady state = TodoFileReady(remotePath: 'path/'); expect(state.remotePath, 'path/'); }); test('remotePath without trailing /', () { TodoFileReady state = TodoFileReady(remotePath: 'path'); expect(state.remotePath, 'path/'); }); test('copyWith() with todoFilename', () { TodoFileReady state = TodoFileReady( todoFilename: 'todo.txt', ); TodoFileReady state2 = state.copyWith(todoFilename: 'todo2.txt'); expect(state2.todoFilename, 'todo2.txt'); }); test('copyWith() with doneFilename', () { TodoFileReady state = TodoFileReady( doneFilename: 'done.txt', ); TodoFileReady state2 = state.copyWith(doneFilename: 'done2.txt'); expect(state2.doneFilename, 'done2.txt'); }); test('copyWith() with localPath', () { TodoFileReady state = TodoFileReady( localPath: '/local', ); TodoFileReady state2 = state.copyWith(localPath: '/local2/'); expect(state2.localPath, '/local2/'); }); test('copyWith() with remotePath', () { TodoFileReady state = TodoFileReady( remotePath: '/remote', ); TodoFileReady state2 = state.copyWith(remotePath: '/remote2/'); expect(state2.remotePath, '/remote2/'); }); test('toString() without trailing /', () { TodoFileReady state = TodoFileReady( todoFilename: 'todo.txt', localPath: '/local', remotePath: '/remote', ); expect('$state', 'TodoFileReady { localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('toString() with trailing /', () { TodoFileReady state = TodoFileReady( todoFilename: 'todo.txt', localPath: '/local/', remotePath: '/remote/', ); expect('$state', 'TodoFileReady { localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('load()', () { TodoFileReady state = TodoFileReady(); TodoFileLoading state2 = state.load(); expect(state2, isA()); }); test('ready()', () { TodoFileReady state = TodoFileReady(); TodoFileReady state2 = state.ready(); expect(state2, isA()); }); test('error()', () { TodoFileReady state = TodoFileReady(); TodoFileError state2 = state.error(message: 'error'); expect(state2, isA()); }); }); group('TodoFileError', () { test('localPath with trailing /', () { TodoFileError state = TodoFileError(localPath: 'path/', message: ''); expect(state.localPath, 'path/'); }); test('localPath without trailing /', () { TodoFileError state = TodoFileError(localPath: 'path', message: ''); expect(state.localPath, 'path/'); }); test('remotePath with trailing /', () { TodoFileError state = TodoFileError(remotePath: 'path/', message: ''); expect(state.remotePath, 'path/'); }); test('remotePath without trailing /', () { TodoFileError state = TodoFileError(remotePath: 'path', message: ''); expect(state.remotePath, 'path/'); }); test('copyWith() with todoFilename', () { TodoFileError state = TodoFileError( message: 'error', todoFilename: 'todo.txt', ); TodoFileError state2 = state.copyWith(todoFilename: 'todo2.txt'); expect(state2.todoFilename, 'todo2.txt'); }); test('copyWith() with doneFilename', () { TodoFileError state = TodoFileError( message: 'error', doneFilename: 'done.txt', ); TodoFileError state2 = state.copyWith(doneFilename: 'done2.txt'); expect(state2.doneFilename, 'done2.txt'); }); test('copyWith() with localPath', () { TodoFileError state = TodoFileError( message: 'error', localPath: '/local', ); TodoFileError state2 = state.copyWith(localPath: '/local2/'); expect(state2.localPath, '/local2/'); }); test('copyWith() with remotePath', () { TodoFileError state = TodoFileError( message: 'error', remotePath: '/remote', ); TodoFileError state2 = state.copyWith(remotePath: '/remote2/'); expect(state2.remotePath, '/remote2/'); }); test('toString() without trailing /', () { TodoFileError state = TodoFileError( message: 'error', todoFilename: 'todo.txt', localPath: '/local', remotePath: '/remote', ); expect('$state', 'TodoFileError { message error localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('toString() with trailing /', () { TodoFileError state = TodoFileError( message: 'error', todoFilename: 'todo.txt', localPath: '/local/', remotePath: '/remote/', ); expect('$state', 'TodoFileError { message error localFile /local/todo.txt remoteFile: /remote/todo.txt }'); }); test('load()', () { TodoFileError state = TodoFileError(message: 'error'); TodoFileLoading state2 = state.load(); expect(state2, isA()); }); test('ready()', () { TodoFileError state = TodoFileError(message: 'error'); TodoFileReady state2 = state.ready(); expect(state2, isA()); }); test('error()', () { TodoFileError state = TodoFileError(message: 'error'); TodoFileError state2 = state.error(message: 'error'); expect(state2, isA()); }); }); } ================================================ FILE: test/webdav/client/webdav_client_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:ntodotxt/webdav/client/webdav_client.dart'; void main() { setUp(() {}); group('scheme', () { test('http', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.scheme, 'http'); }); test('https', () { WebDAVClient client = WebDAVClient( server: 'https://example.org', path: '/', username: 'test', password: 'test', ); expect(client.scheme, 'https'); }); test('undefined', () { expect( () => WebDAVClient( server: 'example.org', path: '/', username: 'test', password: 'test', ), throwsA(isA()), ); }); test('http', () { expect( () => WebDAVClient( server: 'httpx://example.org', path: '/', username: 'test', password: 'test', ), throwsA(isA()), ); }); }); group('host', () { test('valid 1', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.host, 'example.org'); }); test('valid 2', () { WebDAVClient client = WebDAVClient( server: 'http://example-local', path: '/', username: 'test', password: 'test', ); expect(client.host, 'example-local'); }); test('undefined', () { expect( () => WebDAVClient( server: 'httpx://', path: '/', username: 'test', password: 'test', ), throwsA(isA()), ); }); test('invalid', () { expect( () => WebDAVClient( server: 'httpx://example_test.org', path: '/', username: 'test', password: 'test', ), throwsA(isA()), ); }); }); group('port', () { test('valid 1', () { WebDAVClient client = WebDAVClient( server: 'http://example.org:80', path: '/', username: 'test', password: 'test', ); expect(client.port, 80); }); test('valid 2', () { WebDAVClient client = WebDAVClient( server: 'http://example.org:8443', path: '/', username: 'test', password: 'test', ); expect(client.port, 8443); }); test('undefined', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.port, null); }); test('invalid', () { expect( () => WebDAVClient( server: 'httpx://example.org:abc', path: '/', username: 'test', password: 'test', ), throwsA(isA()), ); }); }); group('path', () { test('valid 1', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '', username: 'test', password: 'test', ); expect(client.path, ''); }); test('valid 2', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.path, '/'); }); test('valid 3', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/test', username: 'test', password: 'test', ); expect(client.path, '/test'); }); test('valid 4', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/test/', username: 'test', password: 'test', ); expect(client.path, '/test'); }); }); group('username', () { test('valid', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.username, 'test'); }); }); group('password', () { test('valid', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.password, 'test'); }); }); group('acceptUntrustedCert', () { test('true', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', acceptUntrustedCert: true, ); expect(client.acceptUntrustedCert, true); }); test('false', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', acceptUntrustedCert: false, ); expect(client.acceptUntrustedCert, false); }); test('undefined', () { WebDAVClient client = WebDAVClient( server: 'http://example.org', path: '/', username: 'test', password: 'test', ); expect(client.acceptUntrustedCert, false); }); }); } ================================================ FILE: test_driver/screenshot_integration_test.dart ================================================ import 'dart:io'; import 'package:integration_test/integration_test_driver_extended.dart'; Future main() async { try { await integrationDriver( onScreenshot: (String screenshotName, List screenshotBytes, [Map? args]) async { final File image = await File('screenshots/$screenshotName.png') .create(recursive: true); image.writeAsBytesSync(screenshotBytes); return true; }, ); } catch (e) { // ignore: avoid_print print('Error occured: $e'); } } ================================================ FILE: web/index.html ================================================ ntodotxt ================================================ FILE: web/manifest.json ================================================ { "name": "ntodotxt", "short_name": "ntodotxt", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: windows/CMakeLists.txt ================================================ # Project-level configuration. cmake_minimum_required(VERSION 3.14) project(ntodotxt LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. set(BINARY_NAME "ntodotxt") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() # Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. # # Be cautious about adding new options here, as plugins use this function by # default. In most cases, you should add new options to specific targets instead # of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() # Flutter library and tool build rules. set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") install(DIRECTORY "${NATIVE_ASSETS_DIR}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: windows/flutter/CMakeLists.txt ================================================ # This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # Set fallback configurations for older versions of the flutter tool. if (NOT DEFINED FLUTTER_TARGET_PLATFORM) set(FLUTTER_TARGET_PLATFORM "windows-x64") endif() # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) # Define the application target. To change its name, change BINARY_NAME in the # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer # work. # # Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) # Add preprocessor definitions for the build version. target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") # Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else #define VERSION_AS_NUMBER 1,0,0,0 #endif #if defined(FLUTTER_VERSION) #define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "de.tnmgl" "\0" VALUE "FileDescription", "ntodotxt" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "ntodotxt" "\0" VALUE "LegalCopyright", "Copyright (C) 2025 de.tnmgl. All rights reserved." "\0" VALUE "OriginalFilename", "ntodotxt.exe" "\0" VALUE "ProductName", "ntodotxt" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); // Flutter can complete the first frame before the "show window" callback is // registered. The following call ensures a frame is pending to ensure the // window is shown. It is a no-op if the first frame hasn't completed yet. flutter_controller_->ForceRedraw(); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.Create(L"ntodotxt", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include #include "resource.h" namespace { /// Window attribute that enables dark mode window decorations. /// /// Redefined in case the developer's machine has a Windows SDK older than /// version 10.0.22000.0. /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; /// Registry key for app theme preference. /// /// A value of 0 indicates apps should use dark mode. A non-zero or missing /// value indicates apps should use light mode. constexpr const wchar_t kGetPreferredBrightnessRegKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); } FreeLibrary(user32_module); } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } UpdateTheme(window); return OnCreate(); } bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; case WM_DWMCOLORIZATIONCOLORCHANGED: UpdateTheme(hwnd); return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } void Win32Window::UpdateTheme(HWND const window) { DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, &light_mode, &light_mode_size); if (result == ERROR_SUCCESS) { BOOL enable_dark_mode = light_mode == 0; DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, &enable_dark_mode, sizeof(enable_dark_mode)); } } ================================================ FILE: windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size this function will scale the inputted width and height as // as appropriate for the default monitor. The window is invisible until // |Show| is called. Returns true if the window was created successfully. bool Create(const std::wstring& title, const Point& origin, const Size& size); // Show the current window. Returns true if the window was successfully shown. bool Show(); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; // Update the window frame's theme to match the system theme. static void UpdateTheme(HWND const window); bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_