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
[](https://github.com/tmaegel/ntodotxt/actions/workflows/ci.yaml)
[](https://github.com/tmaegel/ntodotxt/releases)
[](https://f-droid.org/packages/de.tnmgl.ntodotxt)
[](https://opensource.org/licenses/MIT)
[](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
[
](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.
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