├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Icon100.png │ │ │ ├── Icon102.png │ │ │ ├── Icon1024.png │ │ │ ├── Icon114.png │ │ │ ├── Icon120.png │ │ │ ├── Icon128.png │ │ │ ├── Icon144.png │ │ │ ├── Icon152.png │ │ │ ├── Icon16.png │ │ │ ├── Icon167.png │ │ │ ├── Icon172.png │ │ │ ├── Icon180.png │ │ │ ├── Icon196.png │ │ │ ├── Icon20.png │ │ │ ├── Icon216.png │ │ │ ├── Icon256.png │ │ │ ├── Icon29.png │ │ │ ├── Icon32.png │ │ │ ├── Icon40.png │ │ │ ├── Icon48.png │ │ │ ├── Icon50.png │ │ │ ├── Icon512.png │ │ │ ├── Icon55.png │ │ │ ├── Icon57.png │ │ │ ├── Icon58.png │ │ │ ├── Icon60.png │ │ │ ├── Icon64.png │ │ │ ├── Icon66.png │ │ │ ├── Icon72.png │ │ │ ├── Icon76.png │ │ │ ├── Icon80.png │ │ │ ├── Icon87.png │ │ │ ├── Icon88.png │ │ │ ├── Icon92.png │ │ │ ├── Iconwatch.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift └── .gitignore ├── .github └── FUNDING.yml ├── img ├── logo.png ├── background.png ├── foreground.png ├── home-preview.png ├── filter-preview.png ├── add-task-preview.png ├── edit-task-preview.png └── notification-preview.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── doable_todo_list_app │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── assets ├── plus.svg ├── cross.svg ├── twitter.svg ├── filter.svg ├── hamburger.svg ├── trans_logo.svg ├── tick.svg ├── bell.svg ├── bell_white.svg ├── linkedin.svg ├── back.svg ├── search.svg ├── github.svg ├── calendar.svg └── clock.svg ├── .gitignore ├── LICENSE.md ├── lib ├── data │ ├── database_service.dart │ └── task_dao.dart ├── models │ └── task_entity.dart ├── repositories │ └── task_repository.dart ├── services │ └── notification_service.dart ├── main.dart └── screens │ ├── settings_page.dart │ ├── add_task_page.dart │ └── edit_task_page.dart ├── analysis_options.yaml ├── .metadata ├── pubspec.yaml └── README.md /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buymeacoffee: 'https://www.buymeacoffee.com/theakhinabraham' 2 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/background.png -------------------------------------------------------------------------------- /img/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/foreground.png -------------------------------------------------------------------------------- /img/home-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/home-preview.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /img/filter-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/filter-preview.png -------------------------------------------------------------------------------- /img/add-task-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/add-task-preview.png -------------------------------------------------------------------------------- /img/edit-task-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/edit-task-preview.png -------------------------------------------------------------------------------- /img/notification-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/img/notification-preview.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon100.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon102.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon114.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon120.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon128.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon144.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon152.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon16.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon167.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon172.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon180.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon196.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon20.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon216.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon256.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon32.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon48.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon512.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon55.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon58.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon64.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon66.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon66.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon80.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon87.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon88.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon92.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Iconwatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Iconwatch.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theakhinabraham/doable-todo-list-app/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/doable_todo_list_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.doable_todo_list_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /assets/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /assets/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '2.2.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.6.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /assets/trans_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | 22 | } 23 | } 24 | 25 | plugins { 26 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 27 | id "com.android.application" version "8.6.0" apply false 28 | id "org.jetbrains.kotlin.android" version "2.2.10" apply false 29 | } 30 | 31 | include ":app" 32 | -------------------------------------------------------------------------------- /assets/bell_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Akhin Abraham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/data/database_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart' as p; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | class DatabaseService { 5 | static const _dbName = 'doable.db'; 6 | static const _dbVersion = 1; 7 | 8 | static Database? _db; 9 | 10 | static Future instance() async { 11 | if (_db != null) return _db!; 12 | final base = await getDatabasesPath(); 13 | final path = p.join(base, _dbName); 14 | _db = await openDatabase( 15 | path, 16 | version: _dbVersion, 17 | onCreate: (db, version) async { 18 | await db.execute(''' 19 | CREATE TABLE tasks( 20 | id INTEGER PRIMARY KEY AUTOINCREMENT, 21 | title TEXT NOT NULL, 22 | description TEXT, 23 | time TEXT, 24 | date TEXT, 25 | has_notification INTEGER NOT NULL DEFAULT 0, 26 | repeat_rule TEXT, 27 | completed INTEGER NOT NULL DEFAULT 0, 28 | created_at TEXT DEFAULT CURRENT_TIMESTAMP, 29 | updated_at TEXT 30 | ) 31 | '''); 32 | }, 33 | onUpgrade: (db, oldV, newV) async { 34 | // Future migrations go here (use db.execute, db.batch). 35 | }, 36 | ); 37 | return _db!; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /lib/models/task_entity.dart: -------------------------------------------------------------------------------- 1 | class TaskEntity { 2 | TaskEntity({ 3 | this.id, 4 | required this.title, 5 | this.description, 6 | this.time, 7 | this.date, 8 | this.hasNotification = false, 9 | this.repeatRule, 10 | this.completed = false, 11 | this.createdAt, 12 | this.updatedAt, 13 | }); 14 | 15 | int? id; 16 | String title; 17 | String? description; 18 | String? time; // "11:30 AM" 19 | String? date; // "26/11/24" 20 | bool hasNotification; 21 | String? repeatRule; // e.g., "Weekly" 22 | bool completed; 23 | String? createdAt; 24 | String? updatedAt; 25 | 26 | factory TaskEntity.fromMap(Map m) => TaskEntity( 27 | id: m['id'] as int?, 28 | title: m['title'] as String, 29 | description: m['description'] as String?, 30 | time: m['time'] as String?, 31 | date: m['date'] as String?, 32 | hasNotification: (m['has_notification'] as int? ?? 0) == 1, 33 | repeatRule: m['repeat_rule'] as String?, 34 | completed: (m['completed'] as int? ?? 0) == 1, 35 | createdAt: m['created_at'] as String?, 36 | updatedAt: m['updated_at'] as String?, 37 | ); 38 | 39 | Map toMap() => { 40 | 'id': id, 41 | 'title': title, 42 | 'description': description, 43 | 'time': time, 44 | 'date': date, 45 | 'has_notification': hasNotification ? 1 : 0, 46 | 'repeat_rule': repeatRule, 47 | 'completed': completed ? 1 : 0, 48 | 'created_at': createdAt, 49 | 'updated_at': updatedAt, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /assets/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Doable 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Doable 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "41456452f29d64e8deb623a3c927524bcf9f111b" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 17 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 18 | - platform: android 19 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 20 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 21 | - platform: ios 22 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 23 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 24 | - platform: linux 25 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 26 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 27 | - platform: macos 28 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 29 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 30 | - platform: web 31 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 32 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 33 | - platform: windows 34 | create_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 35 | base_revision: 41456452f29d64e8deb623a3c927524bcf9f111b 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /lib/repositories/task_repository.dart: -------------------------------------------------------------------------------- 1 | import '../data/task_dao.dart'; 2 | import '../models/task_entity.dart'; 3 | import '../services/notification_service.dart'; 4 | 5 | class TaskRepository { 6 | Future> fetchAll() => TaskDao.getAll(); 7 | 8 | Future add(TaskEntity t) async { 9 | final id = await TaskDao.insert(t); 10 | t.id = id; 11 | 12 | if (t.hasNotification) { 13 | await NotificationService.scheduleTaskNotification(t); 14 | } 15 | return id; 16 | } 17 | 18 | Future update(TaskEntity t) async { 19 | final result = await TaskDao.update(t); 20 | 21 | if (t.id != null) { 22 | await NotificationService.cancelTaskNotification(t.id); 23 | if (t.hasNotification && !t.completed) { 24 | await NotificationService.scheduleTaskNotification(t); 25 | } 26 | } 27 | return result; 28 | } 29 | 30 | Future delete(int id) async { 31 | // Cancel notification before deleting task 32 | await NotificationService.cancelTaskNotification(id); 33 | return TaskDao.delete(id); 34 | } 35 | 36 | Future toggle(int id, bool value) async { 37 | final result = await TaskDao.toggleCompleted(id, value); 38 | if (value) { 39 | // Cancel notification if task is completed 40 | await NotificationService.cancelTaskNotification(id); 41 | } else { 42 | // Reschedule notification if task is uncompleted 43 | final task = await TaskDao.getById(id); 44 | if (task != null && task.hasNotification) { 45 | await NotificationService.scheduleTaskNotification(task); 46 | } 47 | } 48 | return result; 49 | } 50 | 51 | Future seedDemo() => TaskDao.seedDemo(); 52 | 53 | Future clearAll() async { 54 | // Cancel all notifications first 55 | await NotificationService.cancelAllNotifications(); 56 | return TaskDao.clearAll(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.doable_todo_list_app" 27 | compileSdkVersion 36 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | 34 | coreLibraryDesugaringEnabled true 35 | } 36 | 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.doable_todo_list_app" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion 34 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' 72 | } 73 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 22 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/data/task_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | import '../models/task_entity.dart'; 3 | import 'database_service.dart'; 4 | 5 | class TaskDao { 6 | static const table = 'tasks'; 7 | 8 | static Future insert(TaskEntity t) async { 9 | final db = await DatabaseService.instance(); 10 | t.updatedAt = DateTime.now().toIso8601String(); 11 | return db.insert(table, t.toMap(), conflictAlgorithm: ConflictAlgorithm.replace); 12 | } 13 | 14 | static Future update(TaskEntity t) async { 15 | final db = await DatabaseService.instance(); 16 | t.updatedAt = DateTime.now().toIso8601String(); 17 | return db.update(table, t.toMap(), where: 'id = ?', whereArgs: [t.id]); 18 | } 19 | 20 | static Future toggleCompleted(int id, bool value) async { 21 | final db = await DatabaseService.instance(); 22 | return db.update( 23 | table, 24 | {'completed': value ? 1 : 0, 'updated_at': DateTime.now().toIso8601String()}, 25 | where: 'id = ?', 26 | whereArgs: [id], 27 | ); 28 | } 29 | 30 | static Future delete(int id) async { 31 | final db = await DatabaseService.instance(); 32 | return db.delete(table, where: 'id = ?', whereArgs: [id]); 33 | } 34 | 35 | static Future> getAll() async { 36 | final db = await DatabaseService.instance(); 37 | final rows = await db.query( 38 | table, 39 | orderBy: 'completed ASC, updated_at DESC, id DESC', 40 | ); 41 | return rows.map(TaskEntity.fromMap).toList(); 42 | } 43 | 44 | static Future getById(int id) async { 45 | final db = await DatabaseService.instance(); 46 | final rows = await db.query(table, where: 'id = ?', whereArgs: [id], limit: 1); 47 | if (rows.isEmpty) return null; 48 | return TaskEntity.fromMap(rows.first); 49 | } 50 | 51 | static Future clearAll() async { 52 | final db = await DatabaseService.instance(); 53 | return db.delete('tasks'); 54 | } 55 | 56 | // Optional convenience for seeding demo data 57 | static Future seedDemo() async { 58 | final db = await DatabaseService.instance(); 59 | final count = Sqflite.firstIntValue( 60 | await db.rawQuery('SELECT COUNT(*) FROM $table'), 61 | ) ?? 62 | 0; 63 | if (count > 0) return; 64 | final now = DateTime.now().toIso8601String(); 65 | final batch = db.batch(); 66 | batch.insert(table, { 67 | 'title': 'Return library books', 68 | 'description': 'Gather overdue library books and return…', 69 | 'time': '11:30 AM', 70 | 'date': '26/11/24', 71 | 'has_notification': 1, 72 | 'repeat_rule': 'Weekly', 73 | 'completed': 0, 74 | 'created_at': now, 75 | 'updated_at': now, 76 | }); 77 | batch.insert(table, { 78 | 'title': 'Schedule car maintenance', 79 | 'description': "Check your car's maintenance schedule", 80 | 'time': '3:30 PM', 81 | 'date': '26/11/24', 82 | 'has_notification': 1, 83 | 'repeat_rule': null, 84 | 'completed': 0, 85 | 'created_at': now, 86 | 'updated_at': now, 87 | }); 88 | batch.insert(table, { 89 | 'title': 'Go for grocery shop', 90 | 'description': 'Buy milk, eggs, bread, fruits, and veggies', 91 | 'time': '7:00 PM', 92 | 'date': '26/11/24', 93 | 'has_notification': 0, 94 | 'repeat_rule': 'Daily', 95 | 'completed': 1, 96 | 'created_at': now, 97 | 'updated_at': now, 98 | }); 99 | batch.insert(table, { 100 | 'title': 'Donate unwanted items', 101 | 'description': 'Sort clothes and books; drop off at local charity', 102 | 'time': '5:30 PM', 103 | 'date': '27/11/24', 104 | 'has_notification': 1, 105 | 'repeat_rule': 'monthly', 106 | 'completed': 1, 107 | 'created_at': now, 108 | 'updated_at': now, 109 | }); 110 | await batch.commit(noResult: true); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "scale": "2x", 5 | "size": "20x20", 6 | "idiom": "iphone", 7 | "filename": "Icon40.png" 8 | }, 9 | { 10 | "scale": "3x", 11 | "size": "20x20", 12 | "idiom": "iphone", 13 | "filename": "Icon60.png" 14 | }, 15 | { 16 | "scale": "2x", 17 | "size": "29x29", 18 | "idiom": "iphone", 19 | "filename": "Icon58.png" 20 | }, 21 | { 22 | "scale": "3x", 23 | "size": "29x29", 24 | "idiom": "iphone", 25 | "filename": "Icon87.png" 26 | }, 27 | { 28 | "scale": "2x", 29 | "size": "38x38", 30 | "idiom": "iphone", 31 | "filename": "Icon76.png" 32 | }, 33 | { 34 | "scale": "3x", 35 | "size": "38x38", 36 | "idiom": "iphone", 37 | "filename": "Icon114.png" 38 | }, 39 | { 40 | "scale": "2x", 41 | "size": "40x40", 42 | "idiom": "iphone", 43 | "filename": "Icon80.png" 44 | }, 45 | { 46 | "scale": "3x", 47 | "size": "40x40", 48 | "idiom": "iphone", 49 | "filename": "Icon120.png" 50 | }, 51 | { 52 | "scale": "2x", 53 | "size": "60x60", 54 | "idiom": "iphone", 55 | "filename": "Icon120.png" 56 | }, 57 | { 58 | "scale": "3x", 59 | "size": "60x60", 60 | "idiom": "iphone", 61 | "filename": "Icon180.png" 62 | }, 63 | { 64 | "scale": "1x", 65 | "size": "20x20", 66 | "idiom": "ipad", 67 | "filename": "Icon20.png" 68 | }, 69 | { 70 | "scale": "2x", 71 | "size": "20x20", 72 | "idiom": "ipad", 73 | "filename": "Icon40.png" 74 | }, 75 | { 76 | "scale": "1x", 77 | "size": "29x29", 78 | "idiom": "ipad", 79 | "filename": "Icon29.png" 80 | }, 81 | { 82 | "scale": "2x", 83 | "size": "29x29", 84 | "idiom": "ipad", 85 | "filename": "Icon58.png" 86 | }, 87 | { 88 | "scale": "2x", 89 | "size": "38x38", 90 | "idiom": "ipad", 91 | "filename": "Icon76.png" 92 | }, 93 | { 94 | "scale": "1x", 95 | "size": "40x40", 96 | "idiom": "ipad", 97 | "filename": "Icon40.png" 98 | }, 99 | { 100 | "scale": "2x", 101 | "size": "40x40", 102 | "idiom": "ipad", 103 | "filename": "Icon80.png" 104 | }, 105 | { 106 | "scale": "3x", 107 | "size": "40x40", 108 | "idiom": "ipad", 109 | "filename": "Icon120.png" 110 | }, 111 | { 112 | "scale": "1x", 113 | "size": "76x76", 114 | "idiom": "ipad", 115 | "filename": "Icon76.png" 116 | }, 117 | { 118 | "scale": "2x", 119 | "size": "76x76", 120 | "idiom": "ipad", 121 | "filename": "Icon152.png" 122 | }, 123 | { 124 | "scale": "2x", 125 | "size": "83.5x83.5", 126 | "idiom": "ipad", 127 | "filename": "Icon167.png" 128 | }, 129 | { 130 | "scale": "1x", 131 | "size": "512x512", 132 | "idiom": "mac", 133 | "filename": "Icon512.png" 134 | }, 135 | { 136 | "scale": "2x", 137 | "size": "512x512", 138 | "idiom": "mac", 139 | "filename": "Icon1024.png" 140 | }, 141 | { 142 | "scale": "1x", 143 | "size": "256x256", 144 | "idiom": "mac", 145 | "filename": "Icon256.png" 146 | }, 147 | { 148 | "scale": "2x", 149 | "size": "256x256", 150 | "idiom": "mac", 151 | "filename": "Icon512.png" 152 | }, 153 | { 154 | "scale": "1x", 155 | "size": "128x128", 156 | "idiom": "mac", 157 | "filename": "Icon128.png" 158 | }, 159 | { 160 | "scale": "2x", 161 | "size": "128x128", 162 | "idiom": "mac", 163 | "filename": "Icon256.png" 164 | }, 165 | { 166 | "scale": "1x", 167 | "size": "32x32", 168 | "idiom": "mac", 169 | "filename": "Icon32.png" 170 | }, 171 | { 172 | "scale": "2x", 173 | "size": "32x32", 174 | "idiom": "mac", 175 | "filename": "Icon64.png" 176 | }, 177 | { 178 | "scale": "1x", 179 | "size": "16x16", 180 | "idiom": "mac", 181 | "filename": "Icon16.png" 182 | }, 183 | { 184 | "scale": "2x", 185 | "size": "16x16", 186 | "idiom": "mac", 187 | "filename": "Icon32.png" 188 | }, 189 | { 190 | "scale": "1x", 191 | "size": "1024x1024", 192 | "idiom": "ios-marketing", 193 | "filename": "Icon1024.png" 194 | } 195 | ], 196 | "properties": {}, 197 | "info": { 198 | "version": 1, 199 | "author": "xcode" 200 | } 201 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: doable_todo_list_app 2 | description: "Offline To-do List" 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.2.6 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | flutter_svg: ^2.0.10+1 38 | sqflite: ^2.4.0+1 39 | intl: ^0.20.2 40 | path: 41 | url_launcher: ^6.3.0 42 | shared_preferences: ^2.3.2 43 | timezone: ^0.10.1 44 | awesome_notifications: ^0.10.1 45 | awesome_notifications_core: ^0.10.1 46 | awesome_notifications_fcm: ^0.10.1 47 | 48 | 49 | 50 | dev_dependencies: 51 | flutter_test: 52 | sdk: flutter 53 | 54 | # The "flutter_lints" package below contains a set of recommended lints to 55 | # encourage good coding practices. The lint set provided by the package is 56 | # activated in the `analysis_options.yaml` file located at the root of your 57 | # package. See that file for information about deactivating specific lint 58 | # rules and activating additional ones. 59 | flutter_lints: ^6.0.0 60 | flutter_launcher_icons: ^0.14.1 61 | build_runner: ^2.4.8 62 | 63 | 64 | flutter_launcher_icons: 65 | android: true 66 | ios: true 67 | remove_alpha_ios: true 68 | image_path_android: "img/logo.png" 69 | image_path_ios: "img/logo.png" 70 | adaptive_icon_background: "img/background.png" 71 | adaptive_icon_foreground: "img/foreground.png" 72 | 73 | # For information on the generic Dart part of this file, see the 74 | # following page: https://dart.dev/tools/pub/pubspec 75 | 76 | # The following section is specific to Flutter packages. 77 | flutter: 78 | 79 | # The following line ensures that the Material Icons font is 80 | # included with your application, so that you can use the icons in 81 | # the material Icons class. 82 | uses-material-design: true 83 | 84 | assets: 85 | - assets/ 86 | # To add assets to your application, add an assets section, like this: 87 | # assets: 88 | # - images/a_dot_burr.jpeg 89 | # - images/a_dot_ham.jpeg 90 | 91 | # An image asset can refer to one or more resolution-specific "variants", see 92 | # https://flutter.dev/assets-and-images/#resolution-aware 93 | 94 | # For details regarding adding assets from package dependencies, see 95 | # https://flutter.dev/assets-and-images/#from-packages 96 | 97 | # To add custom fonts to your application, add a fonts section here, 98 | # in this "flutter" section. Each entry in this list should have a 99 | # "family" key with the font family name, and a "fonts" key with a 100 | # list giving the asset and other descriptors for the font. For 101 | # example: 102 | # fonts: 103 | # - family: Schyler 104 | # fonts: 105 | # - asset: fonts/Schyler-Regular.ttf 106 | # - asset: fonts/Schyler-Italic.ttf 107 | # style: italic 108 | # - family: Trajan Pro 109 | # fonts: 110 | # - asset: fonts/TrajanPro.ttf 111 | # - asset: fonts/TrajanPro_Bold.ttf 112 | # weight: 700 113 | # 114 | # For details regarding fonts from package dependencies, 115 | # see https://flutter.dev/custom-fonts/#from-packages 116 | -------------------------------------------------------------------------------- /lib/services/notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:awesome_notifications/awesome_notifications.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import '../models/task_entity.dart'; 6 | 7 | class NotificationService { 8 | static const String channelKey = 'task_reminders'; 9 | static const String _prefsKeyNotifications = 'notifications_enabled'; 10 | 11 | 12 | static Future areNotificationsEnabledByUser() async { 13 | final prefs = await SharedPreferences.getInstance(); 14 | return prefs.getBool(_prefsKeyNotifications) ?? false; 15 | } 16 | 17 | /// Schedule a notification for a task 18 | static Future scheduleTaskNotification(TaskEntity task) async { 19 | // Check if user has enabled notifications 20 | final userEnabled = await areNotificationsEnabledByUser(); 21 | if (!userEnabled) { 22 | print('Notification not scheduled: user has disabled notifications'); 23 | return; 24 | } 25 | 26 | if (!task.hasNotification || task.time == null || task.date == null) { 27 | print('Notification not scheduled: hasNotification=${task.hasNotification}, time=${task.time}, date=${task.date}'); 28 | return; 29 | } 30 | 31 | try { 32 | // Parse the date and time 33 | final dateFormat = DateFormat('dd/MM/yy'); 34 | final timeFormat = DateFormat('h:mm a'); 35 | 36 | final date = dateFormat.parse(task.date!); 37 | final time = timeFormat.parse(task.time!); 38 | 39 | // Combine date and time 40 | final scheduledDateTime = DateTime( 41 | date.year, 42 | date.month, 43 | date.day, 44 | time.hour, 45 | time.minute, 46 | ); 47 | 48 | print('Scheduling notification for ${task.title} at $scheduledDateTime'); 49 | 50 | // Don't schedule if the time has already passed 51 | if (scheduledDateTime.isBefore(DateTime.now())) { 52 | print('Notification not scheduled: time has already passed'); 53 | return; 54 | } 55 | 56 | await AwesomeNotifications().createNotification( 57 | content: NotificationContent( 58 | id: task.id ?? DateTime.now().millisecondsSinceEpoch ~/ 1000, // Use timestamp if ID is null 59 | channelKey: channelKey, 60 | title: 'Task Reminder', 61 | body: task.title, 62 | category: NotificationCategory.Reminder, 63 | wakeUpScreen: true, 64 | payload: { 65 | 'task_id': task.id?.toString() ?? '', 66 | 'task_title': task.title, 67 | }, 68 | ), 69 | schedule: NotificationCalendar.fromDate( 70 | date: scheduledDateTime, 71 | allowWhileIdle: true, 72 | ), 73 | ); 74 | } catch (e) { 75 | print('Error scheduling notification: $e'); 76 | } 77 | } 78 | 79 | /// Cancel a specific task notification 80 | static Future cancelTaskNotification(int? taskId) async { 81 | if (taskId != null) { 82 | await AwesomeNotifications().cancel(taskId); 83 | } 84 | } 85 | 86 | /// Cancel all notifications 87 | static Future cancelAllNotifications() async { 88 | await AwesomeNotifications().cancelAll(); 89 | } 90 | 91 | /// Reschedule all pending task notifications 92 | static Future rescheduleAllNotifications(List tasks) async { 93 | // Check if user has enabled notifications 94 | final userEnabled = await areNotificationsEnabledByUser(); 95 | if (!userEnabled) { 96 | print('Not rescheduling notifications: user has disabled notifications'); 97 | await cancelAllNotifications(); 98 | return; 99 | } 100 | 101 | // Cancel all existing notifications first 102 | await cancelAllNotifications(); 103 | 104 | // Schedule notifications for all pending tasks 105 | for (final task in tasks) { 106 | if (!task.completed && task.hasNotification) { 107 | await scheduleTaskNotification(task); 108 | } 109 | } 110 | } 111 | 112 | /// Initialize notifications 113 | static Future initializeNotifications() async { 114 | await AwesomeNotifications().initialize( 115 | null, // Use default icon 116 | [ 117 | NotificationChannel( 118 | channelKey: channelKey, 119 | channelName: 'Task Reminders', 120 | channelDescription: 'Reminders for your tasks', 121 | defaultColor: const Color(0xFF9D50DD), 122 | ledColor: const Color(0xFF9D50DD), 123 | importance: NotificationImportance.High, 124 | channelShowBadge: true, 125 | playSound: true, 126 | enableVibration: true, 127 | enableLights: true, 128 | ), 129 | ], 130 | ); 131 | } 132 | 133 | /// Request notification permissions 134 | static Future requestPermissions() async { 135 | final isAllowed = await AwesomeNotifications().isNotificationAllowed(); 136 | if (!isAllowed) { 137 | final result = await AwesomeNotifications().requestPermissionToSendNotifications(); 138 | return result; 139 | } 140 | return true; 141 | } 142 | 143 | /// Check if notifications are allowed 144 | static Future isNotificationAllowed() async { 145 | return await AwesomeNotifications().isNotificationAllowed(); 146 | } 147 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:awesome_notifications/awesome_notifications.dart'; 2 | import 'package:doable_todo_list_app/services/notification_service.dart'; 3 | import 'package:doable_todo_list_app/repositories/task_repository.dart'; 4 | import 'package:doable_todo_list_app/screens/add_task_page.dart'; 5 | import 'package:doable_todo_list_app/screens/edit_task_page.dart'; 6 | import 'package:doable_todo_list_app/screens/home_page.dart'; 7 | import 'package:doable_todo_list_app/screens/settings_page.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | 11 | //Colors 12 | Color blackColor = const Color(0xff0c120c); 13 | Color blueColor = const Color(0xff4285F4); 14 | Color whiteColor = const Color(0xffFDFDFF); 15 | Color iconColor = const Color(0xff565656); 16 | Color outlineColor = const Color(0xffD6D6D6); 17 | Color descriptionColor = const Color(0xff565656); 18 | 19 | // TODO: ADD A .env file 20 | Future main() async { 21 | WidgetsFlutterBinding.ensureInitialized(); // Ensure Flutter is ready 22 | 23 | // Initialize Awesome Notifications 24 | await AwesomeNotifications().initialize( 25 | null, // Use default icon 26 | [ 27 | NotificationChannel( 28 | channelKey: 'task_reminders', 29 | channelName: 'Task Reminders', 30 | channelDescription: 'Notifications for task reminders', 31 | defaultColor: blueColor, 32 | ledColor: blueColor, 33 | importance: NotificationImportance.High, 34 | channelShowBadge: true, 35 | playSound: true, 36 | enableVibration: true, 37 | ), 38 | ], 39 | ); 40 | 41 | // Request notification permissions 42 | final hasPermission = await NotificationService.requestPermissions(); 43 | if (!hasPermission) { 44 | print('Notification permission denied'); 45 | } else { 46 | print('Notification permission granted'); 47 | } 48 | 49 | // Reschedule pending notifications 50 | try { 51 | final tasks = await TaskRepository().fetchAll(); 52 | print('Rescheduling ${tasks.length} tasks'); 53 | await NotificationService.rescheduleAllNotifications(tasks); 54 | print('Notifications rescheduled successfully'); 55 | } catch (e) { 56 | print('Error rescheduling notifications: $e'); 57 | } 58 | 59 | //status bar & navigation bar colors and themes 60 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( 61 | statusBarColor: whiteColor, 62 | statusBarIconBrightness: Brightness.dark, 63 | systemNavigationBarColor: whiteColor, 64 | systemNavigationBarIconBrightness: Brightness.dark, 65 | systemNavigationBarDividerColor: whiteColor)); 66 | 67 | runApp(const DoableApp()); 68 | } 69 | 70 | class DoableApp extends StatefulWidget { 71 | const DoableApp({super.key}); 72 | 73 | static final GlobalKey navigatorKey = GlobalKey(); 74 | 75 | @override 76 | State createState() => _DoableAppState(); 77 | } 78 | 79 | class _DoableAppState extends State { 80 | @override 81 | void initState() { 82 | super.initState(); 83 | 84 | // Set up notification listeners 85 | AwesomeNotifications().setListeners( 86 | onActionReceivedMethod: onActionReceivedMethod, 87 | onNotificationCreatedMethod: onNotificationCreatedMethod, 88 | onNotificationDisplayedMethod: onNotificationDisplayedMethod, 89 | onDismissActionReceivedMethod: onDismissActionReceivedMethod, 90 | ); 91 | } 92 | 93 | @pragma("vm:entry-point") 94 | static Future onActionReceivedMethod(ReceivedAction receivedAction) async { 95 | // Handle notification tap 96 | if (receivedAction.payload?['task_id'] != null) { 97 | // Navigate to home page to show the task 98 | DoableApp.navigatorKey.currentState?.pushNamedAndRemoveUntil( 99 | 'home', 100 | (route) => false, 101 | ); 102 | } 103 | } 104 | 105 | @pragma("vm:entry-point") 106 | static Future onNotificationCreatedMethod(ReceivedNotification receivedNotification) async { 107 | // Handle notification created 108 | } 109 | 110 | @pragma("vm:entry-point") 111 | static Future onNotificationDisplayedMethod(ReceivedNotification receivedNotification) async { 112 | // Handle notification displayed 113 | } 114 | 115 | @pragma("vm:entry-point") 116 | static Future onDismissActionReceivedMethod(ReceivedAction receivedAction) async { 117 | // Handle notification dismissed 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | return MaterialApp( 123 | navigatorKey: DoableApp.navigatorKey, 124 | home: const HomePage(), 125 | debugShowCheckedModeBanner: false, 126 | theme: ThemeData( 127 | //colors 128 | splashColor: Colors.transparent, 129 | focusColor: Colors.transparent, 130 | hoverColor: Colors.transparent, 131 | //font family 132 | fontFamily: "Inter", 133 | textTheme: const TextTheme( 134 | //Main heading font style - "Create to-do, Modify to-do" 135 | displayLarge: TextStyle( 136 | fontSize: 28.0, 137 | fontWeight: FontWeight.w900, 138 | color: Color(0xff0c120c)), 139 | //Subheading font style - "Today, Settings" 140 | displayMedium: TextStyle( 141 | fontSize: 20.0, 142 | fontWeight: FontWeight.w600, 143 | color: Color(0xff0c120c)), 144 | //Regular app font style - "Set Reminder, Daily, Save, License, ..." 145 | displaySmall: TextStyle( 146 | fontSize: 15.0, 147 | fontWeight: FontWeight.w500, 148 | color: Color(0xff0c120c)), 149 | //box heading font style - "Tell us about your task, Date & Time, Completion status, ..." 150 | labelSmall: TextStyle( 151 | fontSize: 13.0, 152 | fontWeight: FontWeight.w600, 153 | color: Color(0xff565656)), 154 | //Task list heading font style - "Return Library Book" 155 | bodyLarge: TextStyle( 156 | fontSize: 16.0, 157 | fontWeight: FontWeight.w600, 158 | color: Color(0xff0c120c)), 159 | //Task list description font style - "Gather overdue library books and return..." 160 | bodyMedium: TextStyle( 161 | fontSize: 14.0, 162 | fontWeight: FontWeight.w600, 163 | color: Color(0xff565656)), 164 | //Task list icon text font style - "11:30 AM, 26/11/24" 165 | bodySmall: TextStyle( 166 | fontSize: 12.0, 167 | fontWeight: FontWeight.normal, 168 | color: Color(0xff565656)), 169 | )), 170 | 171 | //routes 172 | initialRoute: 'home', 173 | routes: { 174 | 'home': (context) => const HomePage(), 175 | 'add_task': (context) => const AddTaskPage(), 176 | 'edit_task': (context) => const EditTaskPage(), 177 | 'settings': (context) => const SettingsPage(), 178 | }, 179 | ); 180 | } 181 | } 182 | 183 | double verticalPadding(BuildContext context) { 184 | return MediaQuery.of(context).size.height / 20; 185 | } 186 | 187 | double horizontalPadding(BuildContext context) { 188 | return MediaQuery.of(context).size.width / 20; 189 | } 190 | 191 | EdgeInsets textFieldPadding(BuildContext context) { 192 | // TODO: Convert 25px into respected MediaQuery size 193 | return EdgeInsets.symmetric( 194 | horizontal: MediaQuery.of(context).size.width * 0.1, 195 | vertical: MediaQuery.of(context).size.height * 0.025); 196 | } 197 | 198 | -------------------------------------------------------------------------------- /lib/screens/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'package:awesome_notifications/awesome_notifications.dart'; 6 | 7 | import 'package:doable_todo_list_app/services/notification_service.dart'; 8 | import 'package:doable_todo_list_app/repositories/task_repository.dart'; 9 | 10 | import '../main.dart'; 11 | 12 | class SettingsPage extends StatefulWidget { 13 | const SettingsPage({super.key}); 14 | 15 | @override 16 | State createState() => _SettingsPageState(); 17 | } 18 | 19 | class _SettingsPageState extends State { 20 | static const _prefsKeyNotifications = 'notifications_enabled'; 21 | 22 | bool _notificationsEnabled = false; 23 | bool _loading = true; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | _loadPrefs(); 29 | } 30 | 31 | Future _loadPrefs() async { 32 | final prefs = await SharedPreferences.getInstance(); 33 | final enabled = prefs.getBool(_prefsKeyNotifications) ?? false; 34 | setState(() { 35 | _notificationsEnabled = enabled; 36 | _loading = false; 37 | }); 38 | } 39 | 40 | Future _setNotifications(bool value) async { 41 | // Ask permission when enabling; if denied, keep it disabled. 42 | if (value) { 43 | final granted = await _requestNotificationPermission(); 44 | if (!mounted) return; 45 | if (!granted) { 46 | // Inform user and keep toggle off 47 | ScaffoldMessenger.of(context).showSnackBar( 48 | const SnackBar(content: Text('Notification permission denied')), 49 | ); 50 | value = false; 51 | } else { 52 | 53 | try { 54 | final tasks = await TaskRepository().fetchAll(); 55 | await NotificationService.rescheduleAllNotifications(tasks); 56 | ScaffoldMessenger.of(context).showSnackBar( 57 | const SnackBar(content: Text('Notifications enabled and scheduled')), 58 | ); 59 | } catch (e) { 60 | print('Error rescheduling notifications: $e'); 61 | } 62 | } 63 | } else { 64 | // When disabling notifications, cancel all scheduled notifications 65 | try { 66 | await NotificationService.cancelAllNotifications(); 67 | ScaffoldMessenger.of(context).showSnackBar( 68 | const SnackBar(content: Text('All notifications cancelled')), 69 | ); 70 | } catch (e) { 71 | print('Error cancelling notifications: $e'); 72 | } 73 | } 74 | 75 | final prefs = await SharedPreferences.getInstance(); 76 | await prefs.setBool(_prefsKeyNotifications, value); 77 | if (!mounted) return; 78 | setState(() => _notificationsEnabled = value); 79 | } 80 | 81 | // Request notification permission using awesome_notifications 82 | Future _requestNotificationPermission() async { 83 | return await NotificationService.requestPermissions(); 84 | } 85 | 86 | Future _sendTestNotification() async { 87 | // Check both system permission and user preference 88 | final isAllowed = await NotificationService.isNotificationAllowed(); 89 | if (!isAllowed) { 90 | if (!mounted) return; 91 | ScaffoldMessenger.of(context).showSnackBar( 92 | const SnackBar(content: Text('Notification permission required')), 93 | ); 94 | return; 95 | } 96 | 97 | if (!_notificationsEnabled) { 98 | if (!mounted) return; 99 | ScaffoldMessenger.of(context).showSnackBar( 100 | const SnackBar(content: Text('Notifications are disabled in settings')), 101 | ); 102 | return; 103 | } 104 | 105 | await AwesomeNotifications().createNotification( 106 | content: NotificationContent( 107 | id: 999999, // Use a high ID for test notifications 108 | channelKey: NotificationService.channelKey, 109 | title: 'Test Notification', 110 | body: 'This is a test notification to verify notifications are working!', 111 | category: NotificationCategory.Reminder, 112 | payload: {'test': 'true'}, 113 | ), 114 | ); 115 | 116 | if (!mounted) return; 117 | ScaffoldMessenger.of(context).showSnackBar( 118 | const SnackBar(content: Text('Test notification sent')), 119 | ); 120 | } 121 | 122 | Future _confirmAndClearAll() async { 123 | final confirm = await showDialog( 124 | context: context, 125 | builder: (ctx) => AlertDialog( 126 | title: const Text('Clear all data?'), 127 | content: const Text('This will delete all tasks and reset the app to a fresh state. This action cannot be undone.'), 128 | actions: [ 129 | TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), 130 | FilledButton( 131 | style: FilledButton.styleFrom(backgroundColor: Colors.black), 132 | onPressed: () => Navigator.pop(ctx, true), 133 | child: const Text('Clear'), 134 | ), 135 | ], 136 | ), 137 | ); 138 | 139 | if (confirm != true) return; 140 | 141 | // Wipe the tasks table 142 | await TaskRepository().clearAll(); 143 | 144 | if (!mounted) return; 145 | ScaffoldMessenger.of(context).showSnackBar( 146 | const SnackBar(content: Text('All data cleared')), 147 | ); 148 | } 149 | 150 | Future _openUrl(String url) async { 151 | final uri = Uri.parse(url); 152 | if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { 153 | if (!mounted) return; 154 | ScaffoldMessenger.of(context).showSnackBar( 155 | SnackBar(content: Text('Could not open $url')), 156 | ); 157 | } 158 | } 159 | 160 | EdgeInsets get _screenHPad { 161 | final w = MediaQuery.of(context).size.width; 162 | final hpad = (w * 0.05).clamp(16.0, 24.0); 163 | return EdgeInsets.symmetric(horizontal: hpad); 164 | } 165 | 166 | @override 167 | Widget build(BuildContext context) { 168 | final version = '1.0.0'; 169 | return Scaffold( 170 | backgroundColor: Colors.white, 171 | appBar: AppBar( 172 | backgroundColor: Colors.white, 173 | elevation: 0, 174 | surfaceTintColor: Colors.white, 175 | leading: IconButton( 176 | onPressed: () => Navigator.pop(context), 177 | icon: const Icon(Icons.arrow_back, color: Colors.black), 178 | tooltip: 'Back', 179 | ), 180 | title: const Text( 181 | 'Settings', 182 | style: TextStyle( 183 | fontSize: 24, 184 | fontWeight: FontWeight.w800, 185 | color: Colors.black, 186 | ), 187 | ), 188 | centerTitle: false, 189 | ), 190 | body: SafeArea( 191 | child: _loading 192 | ? const Center(child: CircularProgressIndicator()) 193 | : ListView( 194 | padding: _screenHPad.add(const EdgeInsets.only(bottom: 24, top: 8)), 195 | children: [ 196 | // Notifications toggle 197 | Row( 198 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 199 | children: [ 200 | Text( 201 | 'Notifications', 202 | style: TextStyle(fontSize: 16, color: blackColor, fontWeight: FontWeight.w600), 203 | ), 204 | Switch( 205 | value: _notificationsEnabled, 206 | onChanged: _setNotifications, 207 | activeThumbColor: blueColor, 208 | ), 209 | ], 210 | ), 211 | const SizedBox(height: 16), 212 | 213 | // Clear All Data pill button 214 | SizedBox( 215 | height: 48, 216 | child: FilledButton( 217 | style: FilledButton.styleFrom( 218 | backgroundColor: blackColor, 219 | shape: RoundedRectangleBorder( 220 | borderRadius: BorderRadius.circular(24), 221 | ), 222 | textStyle: const TextStyle( 223 | fontWeight: FontWeight.w700, 224 | fontSize: 16, 225 | ), 226 | ), 227 | onPressed: _confirmAndClearAll, 228 | child: const Text('Clear All Data'), 229 | ), 230 | ), 231 | 232 | const SizedBox(height: 24), 233 | const Divider(height: 1), 234 | 235 | // About section 236 | const SizedBox(height: 16), 237 | Row( 238 | children: [ 239 | Expanded( 240 | child: Text( 241 | 'License', 242 | style: TextStyle(fontSize: 14, color: descriptionColor, fontWeight: FontWeight.w600), 243 | ), 244 | ), 245 | Text('MIT', style: TextStyle(fontSize: 14, color: descriptionColor, fontWeight: FontWeight.w700)), 246 | ], 247 | ), 248 | const SizedBox(height: 12), 249 | Row( 250 | children: [ 251 | Expanded( 252 | child: Text( 253 | 'Version', 254 | style: TextStyle(fontSize: 14, color: descriptionColor, fontWeight: FontWeight.w600), 255 | ), 256 | ), 257 | Text(version, style: TextStyle(fontSize: 14, color: descriptionColor, fontWeight: FontWeight.w700)), 258 | ], 259 | ), 260 | 261 | // Spacer 262 | SizedBox(height: MediaQuery.of(context).size.height * 0.12), 263 | 264 | // Centered logo + version 265 | Column( 266 | mainAxisSize: MainAxisSize.min, 267 | children: [ 268 | SvgPicture.asset('assets/trans_logo.svg', height: 56), 269 | const SizedBox(height: 8), 270 | Text( 271 | 'Version $version', 272 | style: TextStyle(fontSize: 12, color: descriptionColor, fontWeight: FontWeight.w600), 273 | ), 274 | const SizedBox(height: 32), 275 | 276 | // Social buttons row 277 | Row( 278 | mainAxisAlignment: MainAxisAlignment.center, 279 | children: [ 280 | _SocialIconButton( 281 | asset: 'assets/twitter.svg', 282 | tooltip: 'X', 283 | onTap: () => _openUrl('https://x.com/AkhinAbr'), 284 | ), 285 | const SizedBox(width: 16), 286 | _SocialIconButton( 287 | asset: 'assets/github.svg', 288 | tooltip: 'GitHub', 289 | onTap: () => _openUrl('https://github.com/theakhinabraham'), 290 | ), 291 | const SizedBox(width: 16), 292 | _SocialIconButton( 293 | asset: 'assets/linkedin.svg', 294 | tooltip: 'LinkedIn', 295 | onTap: () => _openUrl('https://www.linkedin.com/in/theakhinabraham'), 296 | ), 297 | ], 298 | ), 299 | ], 300 | ), 301 | ], 302 | ), 303 | ), 304 | ); 305 | } 306 | } 307 | 308 | class _SocialIconButton extends StatelessWidget { 309 | const _SocialIconButton({ 310 | required this.asset, 311 | required this.onTap, 312 | required this.tooltip, 313 | }); 314 | 315 | final String asset; 316 | final VoidCallback onTap; 317 | final String tooltip; 318 | 319 | @override 320 | Widget build(BuildContext context) { 321 | return InkResponse( 322 | onTap: onTap, 323 | radius: 28, 324 | customBorder: const CircleBorder(), 325 | child: Container( 326 | width: 50, 327 | height: 50, 328 | decoration: BoxDecoration( 329 | shape: BoxShape.circle, 330 | color: Colors.black.withOpacity(0.04), 331 | ), 332 | alignment: Alignment.center, 333 | child: Tooltip( 334 | message: tooltip, 335 | child: SvgPicture.asset( 336 | asset, 337 | height: 32, 338 | width: 32, 339 | //colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), 340 | ), 341 | ), 342 | ), 343 | ); 344 | } 345 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | Logo 7 | 8 | 9 |

Doable Todo List App

10 | 11 |

12 | 💡 An offline Flutter todo list app using Dart & SQLite Database with notification reminders & latest UI designs. 13 |

14 |
15 | Report Bugs 16 | · 17 | Request Features 18 |

19 | 20 | ![GitHub repo size](https://img.shields.io/github/repo-size/theakhinabraham/doable-todo-list-app) 21 | ![GitHub contributors](https://img.shields.io/github/contributors/theakhinabraham/doable-todo-list-app) 22 | ![GitHub stars](https://img.shields.io/github/stars/theakhinabraham/doable-todo-list-app?style=social) 23 | ![GitHub forks](https://img.shields.io/github/forks/theakhinabraham/doable-todo-list-app?style=social) 24 | ![Twitter Follow](https://img.shields.io/twitter/follow/akhinabr?style=social) 25 |
26 | [![Flutter](https://img.shields.io/badge/Flutter-3.0+-02569B?style=flat&logo=flutter)](https://flutter.dev) 27 | [![Dart](https://img.shields.io/badge/Dart-3.2.6+-0175C2?style=flat&logo=dart)](https://dart.dev) 28 | [![SQLite](https://img.shields.io/badge/SQLite-003B57?style=flat&logo=sqlite)](https://www.sqlite.org) 29 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 | Table of Contents 42 |
    43 |
  1. 44 | About The Project 45 | 48 |
  2. 49 |
  3. 50 | Getting Started 51 | 55 |
  4. 56 |
  5. Usage
  6. 57 |
  7. Roadmap
  8. 58 |
  9. Contributing
  10. 59 |
  11. License
  12. 60 |
  13. Contact
  14. 61 |
  15. Acknowledgments
  16. 62 |
63 |
64 | 65 |
66 | 67 | 68 | # About Doable: Todo List App 69 | An offline, single‑device to‑do app built with Flutter and SQLite. Features task management with reminders, date/time pickers, and filtering capabilities. 70 |

71 | 72 | ## 📱 Features 73 | 74 | - ✅ **Task Management** - Create, edit, delete tasks with title and optional description 75 | - ⏰ **Smart Scheduling** - Date and time pickers with reminder notifications 76 | - 🔄 **Repeat Rules** - Daily, Weekly, Monthly options with custom weekday selection 77 | - 🔍 **Advanced Filtering** - Filter by date, time, completion status, repeat rules, and reminders 78 | - 📱 **Intuitive UI** - Swipe-to-delete completed tasks, clean Material Design 79 | - 💾 **Offline First** - Local SQLite storage, no network required 80 | - 🗑️ **Data Management** - Clear all data option in settings 81 | 82 |

(back to top)

83 | 84 | 85 | 86 | ### Built With Flutter 87 | 88 | Flutter is an open source framework developed and supported by Google. App Developers use Flutter to build mobile apps for multiple platforms (iOS/android) with a single codebase.

89 | 90 | 91 |

(back to top)

92 | 93 | 94 | 95 | 96 | ## Getting Started 97 | 98 | This is an example of how you may give instructions on setting up your project locally. To get a local copy up and running follow these simple example steps. 99 | 100 | ### Prerequisites 101 | 102 | - [Flutter SDK](https://flutter.dev/docs/get-started/install) (3.0+) 103 | - [Dart SDK](https://dart.dev/get-dart) (3.2.6+) 104 | - Android Studio or VS Code with Flutter extension 105 | 106 | Make sure you have `git` installed, type `git --version` in your cmd. (git official download page: https://git-scm.com/downloads) 107 | 108 | ``` 109 | git --version 110 | ``` 111 | 112 | ### Installation 113 | 114 | Steps to install code and app into your local device (and run app on emulator or mobile device) 115 | 116 | 1. Fork this repository (and leave a star if you like) by click on the `fork` button on the top right side. 117 | 2. From your copy of this repo located `yourname/doable-todo-list-app`, copy the code link: `https://github.com/your-user-name/doable-todo-list-app.git` 118 | 3. Create a folder named `doable` in your local device to store these files. 119 | 4. Open terminal and type `cd path/to/doable` (replace `/path/to/doable` with the real path to the new `doable` folder we created in step 3) 120 | 121 | ``` 122 | cd path/to/doable 123 | ``` 124 |
125 | 5. Clone your copy of this repo using `git clone link-you-copied-in-step-2` 126 | 127 | ``` 128 | git clone https://github.com/your_username_/doable-todo-list-app.git 129 | ``` 130 |
131 | 132 | 6. Open the folder `doable/doable-todo-list-app` in your code editor (I use VS Code) & start coding. 133 | 7. Run `emulator` or connect your mobile device with cable to run app on mobile. 134 | 135 | 136 |

(back to top)

137 | 138 | ## 🏗️ Tech Stack 139 | 140 | | Technology | Purpose | Version | 141 | |------------|---------|---------| 142 | | [Flutter](https://flutter.dev) | UI Framework | 3.0+ | 143 | | [SQLite](https://www.sqlite.org) | Local Database | via sqflite ^2.4.0 | 144 | | [SharedPreferences](https://pub.dev/packages/shared_preferences) | Settings Storage | ^2.3.2 | 145 | | [flutter_svg](https://pub.dev/packages/flutter_svg) | Vector Graphics | ^2.0.10 | 146 | | [intl](https://pub.dev/packages/intl) | Date Formatting | ^0.20.2 | 147 | | [url_launcher](https://pub.dev/packages/url_launcher) | External Links | ^6.3.0 | 148 | 149 | 150 | ## 📂 Project Structure 151 | 152 | lib/
153 | ├── main.dart # App entry point
154 | ├── screens/ # UI screens
155 | │ ├── home_page.dart # Main task list
156 | │ ├── add_task_page.dart # Create new tasks
157 | │ ├── edit_task_page.dart # Modify existing tasks
158 | │ └── settings_page.dart # App settings
159 | ├── data/ # Data layer
160 | │ ├── database_service.dart # SQLite management
161 | │ └── task_dao.dart # Database operations
162 | ├── models/ # Data models
163 | │ └── task_entity.dart # Task data structure
164 | └── task_repository.dart # Repository pattern facade
165 | 166 | 167 | ## 🗄️ Database Schema 168 | 169 | ### Tasks Table 170 | 171 | ``` 172 | CREATE TABLE tasks( 173 | id INTEGER PRIMARY KEY AUTOINCREMENT, 174 | title TEXT NOT NULL, 175 | description TEXT, 176 | time TEXT, -- Display format: "11:30 AM" 177 | date TEXT, -- Display format: "26/11/24" 178 | has_notification INTEGER NOT NULL DEFAULT 0, 179 | repeat_rule TEXT, -- "Daily" | "Weekly" | "Monthly" | "Weekly:" 180 | completed INTEGER NOT NULL DEFAULT 0, 181 | created_at TEXT DEFAULT CURRENT_TIMESTAMP, 182 | updated_at TEXT 183 | ); 184 | ``` 185 | 186 | ## 🎨 Architecture 187 | 188 | The app follows a clean layered architecture: 189 | 190 | ``` 191 | graph TB 192 | A[Presentation Layer] --> B[Repository Layer] 193 | B --> C[Data Access Layer] 194 | C --> D[SQLite Database] 195 | ``` 196 | 197 | ``` 198 | A --> E[SharedPreferences] 199 | 200 | subgraph "Presentation Layer" 201 | A1[HomePage] 202 | A2[AddTaskPage] 203 | A3[EditTaskPage] 204 | A4[SettingsPage] 205 | end 206 | 207 | subgraph "Data Layer" 208 | C1[TaskDao] 209 | C2[DatabaseService] 210 | end 211 | ``` 212 | 213 | 214 | ### Key Components 215 | 216 | - **Presentation Layer**: Stateful/Stateless widgets managing UI state 217 | - **Repository Layer**: TaskRepository as a facade for future extensibility 218 | - **Data Access Layer**: TaskDao + DatabaseService for SQLite operations 219 | - **Domain Model**: TaskEntity for data serialization 220 | 221 | ## 🔧 Configuration 222 | 223 | ### Android Setup 224 | ``` 225 | android { 226 | compileSdk 34 227 | defaultConfig { 228 | minSdk 21 229 | targetSdk 34 230 | } 231 | compileOptions { 232 | sourceCompatibility JavaVersion.VERSION_17 233 | targetCompatibility JavaVersion.VERSION_17 234 | } 235 | } 236 | ``` 237 | 238 | 239 | ### Build Variants 240 | - **Debug**: Development with debug symbols 241 | - **Release**: Optimized production build 242 | 243 | ## 🎯 Usage Examples 244 | 245 | ### Creating a Task 246 | ``` 247 | final task = TaskEntity( 248 | title: "Complete project documentation", 249 | description: "Write comprehensive README", 250 | time: "2:30 PM", 251 | date: "27/01/25", 252 | hasNotification: true, 253 | repeatRule: "Daily", 254 | completed: false, 255 | ); 256 | 257 | await TaskDao.insert(task); 258 | ``` 259 | 260 | ### Filtering Tasks 261 | ``` 262 | // Filter by completion status 263 | final incompleteTasks = tasks.where((t) => !t.completed).toList(); 264 | 265 | // Filter by date 266 | final todayTasks = tasks.where((t) => t.date == "27/01/25").toList(); 267 | 268 | // Filter by repeat rule 269 | final weeklyTasks = tasks.where((t) => 270 | t.repeatRule?.startsWith("Weekly") == true 271 | ).toList(); 272 | ``` 273 | 274 | ## 📱 Screenshots 275 | 276 |

277 | 278 |

279 | 280 | 281 | ## Roadmap 282 | ### Version 1.1.0 283 | - [x] Add, edit and remove tasks 284 | - [x] Complete task and undo completion 285 | - [x] Display tasks on home screen 286 | - [x] Completed tasks are striked through, swipe to delete 287 | - [x] Completed tasks move to the bottom 288 | - [x] Repeat feature (daily, weekly, monthly, no repeat) 289 | - [x] Set date and time for task 290 | - [x] Notification reminders 291 | 292 | ### Version 2.0 293 | - [ ] **Cloud Sync** - Optional cloud backup and sync 294 | - [ ] **Rich Notifications** - Local notification scheduling 295 | - [ ] **Advanced Repeats** - Custom repeat patterns 296 | - [ ] **Categories & Tags** - Task organization 297 | - [ ] **Dark Mode** - Theme customization 298 | 299 | ### Version 2.1 300 | - [ ] **Collaboration** - Shared task lists 301 | - [ ] **Analytics** - Productivity insights 302 | - [ ] **Export/Import** - JSON backup functionality 303 | - [ ] **Widget Support** - Home screen widgets 304 | 305 | See the [open issues](https://github.com/theakhinabraham/doable-todo-list-app/issues) for a full list of proposed features (and known issues). 306 | 307 |

(back to top)

308 | 309 | 310 | 311 | 312 | ## Contributing 313 | 314 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 315 | 316 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 317 | Don't forget to give the project a star! Thanks again! 318 |
319 | 320 |

How to get access to my project:

321 | 322 | 1. Fork this repository (and leave a star if you like) by click on the `fork` button on the top right side. 323 | 2. From your copy of this repo located `yourname/doable-todo-list-app`, copy the code link: `https://github.com/your-user-name/doable-todo-list-app.git` 324 | 3. Create a folder named `doable` in your local device to store these files. 325 | 4. Open terminal and type `cd path/to/doable` (replace `/path/to/doable` with the real path to the new `doable` folder we created in step 3) 326 | 327 | ``` 328 | cd path/to/doable 329 | ``` 330 |
331 | 332 | 5. Clone your copy of this repo using `git clone [link you copied in step 2]` 333 | 334 | ``` 335 | git clone https://github.com/your_username_/doable-todo-list-app.git 336 | ``` 337 |
338 | 339 | 6. Navigate to the root folder of this project 340 | 341 | ``` 342 | cd /path/to/doable-todo-list-app 343 | ``` 344 |
345 | 346 | 7. DO **NOT** MAKE CHANGES TO THE main BRANCH, create your own branch and name it your name 347 | 348 | ``` 349 | git branch my-user-name 350 | ``` 351 |
352 | 353 | 8. Confirm that your new branch `my-user-name` is created 354 | 355 | ``` 356 | git branch 357 | ``` 358 |
359 | 360 | 9. Select your new branch `my-user-name` and work on that branch only 361 | 362 | ``` 363 | git checkout my-user-name 364 | ``` 365 |
366 | 367 | 10. Confirm that you are in your branch `my-user-name` and **NOT** on `main` 368 | 369 | ``` 370 | git branch 371 | ``` 372 |
373 | 374 |

Staying up-to-date with original code:

375 | 376 | 1. You have to shift to `main` branch first but do **NOT** `push` to `main` branch 377 | 378 | ``` 379 | git checkout main 380 | ``` 381 |
382 | 383 | 2. Perform a `pull` to stay updated. It must show `Already up-to-date` 384 | 385 | ``` 386 | git pull 387 | ``` 388 |
389 | 390 | 3. Now you have to shift back to your branch `my-user-name` again before you can continue editing code 391 | 392 | ``` 393 | git checkout my-user-name 394 | ``` 395 |
396 | 397 | 4. Perform a `pull` again in `my-user-name` your own branch 398 | 399 | ``` 400 | git pull 401 | ``` 402 |
403 | 404 |

Once you are ready to push the changes, follow these steps:

405 | 406 | 1. Confirm you are in `my-user-name` your own branch 407 | 408 | ``` 409 | git branch 410 | ``` 411 |
412 | 413 | 2. Push the changes 414 | 415 | ``` 416 | git add . 417 | git commit -m "issue #24 fixed" 418 | ``` 419 |
420 | 421 | 3. Choose `git push origin HEAD`, do **NOT** choose `git push origin HEAD:master` 422 | 423 | ``` 424 | git push 425 | git push origin HEAD 426 | ``` 427 |
428 | 429 | 430 |

(back to top)

431 | 432 | 433 | 434 | 435 | ## License 436 | 437 | Distributed under the MIT License. Click [LICENSE.md](https://github.com/theakhinabraham/doable-todo-list-app/blob/main/LICENSE.md) for more information. 438 | 439 |

(back to top)

440 | 441 | ## 🙏 Acknowledgments 442 | 443 | - [Flutter Team](https://flutter.dev) for the amazing framework 444 | - [SQLite](https://www.sqlite.org) for reliable local storage 445 | - [Material Design](https://material.io) for design inspiration 446 | 447 | 448 | ## 📞 Support 449 | 450 | Akhin Abraham - [instagram.com/akhinabr](https://instagram.com/akhinabr) - theakhinabraham@gmail.com 451 | 452 | Repository Link: [https://github.com/theakhinabraham/doable-todo-list-app](https://github.com/theakhinabraham/doable-todo-list-app) 453 | 454 |

(back to top)

455 | 456 | 457 | 458 | 459 | ## Acknowledgments 460 | 461 | Here are some resource links to help with this project and it's contribution: 462 | 463 | * [Flutter Docs](https://docs.flutter.dev/) 464 | * [Flutter Packages](https://pub.dev/) 465 | 466 | 467 |

(back to top)

468 | -------------------------------------------------------------------------------- /lib/screens/add_task_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | // Data layer 6 | import 'package:doable_todo_list_app/models/task_entity.dart'; 7 | import 'package:doable_todo_list_app/repositories/task_repository.dart'; 8 | import 'package:doable_todo_list_app/services/notification_service.dart'; 9 | 10 | class AddTaskPage extends StatefulWidget { 11 | const AddTaskPage({super.key}); 12 | 13 | @override 14 | State createState() => _AddTaskPageState(); 15 | } 16 | 17 | class _AddTaskPageState extends State { 18 | // Controllers 19 | final _titleCtrl = TextEditingController(); 20 | final _descCtrl = TextEditingController(); 21 | 22 | // State 23 | bool _reminder = false; 24 | DateTime? _selectedDate; 25 | TimeOfDay? _selectedTime; 26 | 27 | // Repeat selections 28 | String? _repeatRule; // "Daily" | "Weekly" | "Monthly" | "No repeat" | null 29 | final Set _repeatWeekdays = {}; // 1=Mon ... 7=Sun 30 | 31 | // Colors (replace with Theme if preferred) 32 | static const Color blueColor = Color(0xFF2563EB); // Tailwind-ish blue-600 33 | static const Color black = Colors.black; 34 | static const Color white = Colors.white; 35 | static const double kRadius = 16; 36 | 37 | // Layout helpers 38 | EdgeInsets get _screenHPad { 39 | final w = MediaQuery.of(context).size.width; 40 | final hpad = (w * 0.05).clamp(16.0, 24.0); // 5% with sensible bounds 41 | return EdgeInsets.symmetric(horizontal: hpad); 42 | } 43 | 44 | String _formatDate(DateTime d) => DateFormat('dd/MM/yy').format(d); 45 | String _formatTime(TimeOfDay t) { 46 | final dt = DateTime(0, 1, 1, t.hour, t.minute); 47 | return DateFormat('h:mm a').format(dt); 48 | } 49 | 50 | Future _pickDate() async { 51 | final now = DateTime.now(); 52 | final picked = await showDatePicker( 53 | context: context, 54 | initialDate: _selectedDate ?? now, 55 | firstDate: now.subtract(const Duration(days: 0)), 56 | lastDate: DateTime(now.year + 5), 57 | helpText: 'Select date', 58 | builder: (ctx, child) { 59 | // Responsive dialog density if needed 60 | return child!; 61 | }, 62 | ); 63 | if (picked != null) { 64 | setState(() => _selectedDate = picked); 65 | } 66 | } 67 | 68 | Future _pickTime() async { 69 | final picked = await showTimePicker( 70 | context: context, 71 | initialTime: _selectedTime ?? TimeOfDay.now(), 72 | helpText: 'Select time', 73 | builder: (ctx, child) => child!, 74 | ); 75 | if (picked != null) { 76 | setState(() => _selectedTime = picked); 77 | } 78 | } 79 | 80 | void _toggleReminder() async { 81 | // Check if notifications are enabled by the user 82 | final userEnabled = await NotificationService.areNotificationsEnabledByUser(); 83 | 84 | if (!userEnabled) { 85 | if (!mounted) return; 86 | ScaffoldMessenger.of(context).showSnackBar( 87 | SnackBar( 88 | content: const Text('Notifications are disabled in settings'), 89 | action: SnackBarAction( 90 | label: 'Settings', 91 | onPressed: () { 92 | // Navigate to settings page 93 | Navigator.pushNamed(context, 'settings'); 94 | }, 95 | ), 96 | ), 97 | ); 98 | return; 99 | } 100 | 101 | setState(() => _reminder = !_reminder); 102 | } 103 | 104 | void _selectRepeatRule(String rule) { 105 | setState(() { 106 | _repeatRule = rule; 107 | // If Daily or Monthly or No repeat is selected, clear weekday specific picks. 108 | if (rule != 'Weekly') _repeatWeekdays.clear(); 109 | }); 110 | } 111 | 112 | void _toggleWeekday(int weekday) { 113 | setState(() { 114 | if (_repeatWeekdays.contains(weekday)) { 115 | _repeatWeekdays.remove(weekday); 116 | } else { 117 | _repeatWeekdays.add(weekday); 118 | } 119 | // If any weekday is selected, set repeat to Weekly automatically. 120 | if (_repeatWeekdays.isNotEmpty) _repeatRule = 'Weekly'; 121 | }); 122 | } 123 | 124 | Future _save() async { 125 | final title = _titleCtrl.text.trim(); 126 | if (title.isEmpty) { 127 | ScaffoldMessenger.of(context).showSnackBar( 128 | const SnackBar(content: Text('Please enter a title')), 129 | ); 130 | return; 131 | } 132 | 133 | // Build display strings (store as plain TEXT in DB) 134 | final dateStr = _selectedDate != null ? _formatDate(_selectedDate!) : null; 135 | final timeStr = _selectedTime != null ? _formatTime(_selectedTime!) : null; 136 | 137 | // Repeat rule string: if Weekly + weekdays, serialize as "Weekly:1,2,3" (Mon=1) 138 | String? repeatRule; 139 | if (_repeatRule == null || _repeatRule == 'No repeat') { 140 | repeatRule = null; 141 | } else if (_repeatRule == 'Weekly' && _repeatWeekdays.isNotEmpty) { 142 | repeatRule = 'Weekly:${_repeatWeekdays.toList()..sort()}'; 143 | } else { 144 | repeatRule = _repeatRule; 145 | } 146 | 147 | final entity = TaskEntity( 148 | title: title, 149 | description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(), 150 | time: timeStr, 151 | date: dateStr, 152 | hasNotification: _reminder, 153 | repeatRule: repeatRule, 154 | completed: false, 155 | ); 156 | 157 | await TaskRepository().add(entity); 158 | if (mounted) Navigator.pop(context, true); // return true so Home reloads 159 | } 160 | 161 | @override 162 | void dispose() { 163 | _titleCtrl.dispose(); 164 | _descCtrl.dispose(); 165 | super.dispose(); 166 | } 167 | 168 | @override 169 | Widget build(BuildContext context) { 170 | final spacing = 16.0; 171 | final bigSpacing = 24.0; 172 | final width = MediaQuery.of(context).size.width; 173 | 174 | return Scaffold( 175 | backgroundColor: Colors.white, 176 | appBar: AppBar( 177 | backgroundColor: white, 178 | elevation: 0, 179 | surfaceTintColor: white, 180 | leading: IconButton( 181 | onPressed: () => Navigator.pop(context, false), 182 | icon: const Icon(Icons.arrow_back, color: Colors.black), 183 | tooltip: 'Back', 184 | ), 185 | title: const Text( 186 | 'Create to-do', 187 | style: TextStyle( 188 | fontSize: 24, 189 | fontWeight: FontWeight.w800, 190 | color: Colors.black, 191 | ), 192 | ), 193 | centerTitle: false, 194 | ), 195 | body: SafeArea( 196 | child: SingleChildScrollView( 197 | padding: EdgeInsets.only(bottom: 24).add(_screenHPad), 198 | child: Column( 199 | crossAxisAlignment: CrossAxisAlignment.start, 200 | children: [ 201 | // Set Reminder button 202 | _ReminderButton( 203 | enabled: _reminder, 204 | onTap: _toggleReminder, 205 | ), 206 | SizedBox(height: bigSpacing), 207 | 208 | // Title / Description 209 | const _FieldLabel(text: 'Tell us about your task'), 210 | SizedBox(height: spacing), 211 | _InputField( 212 | controller: _titleCtrl, 213 | hint: 'Title', 214 | textInputAction: TextInputAction.next, 215 | ), 216 | SizedBox(height: spacing), 217 | _InputField( 218 | controller: _descCtrl, 219 | hint: 'Description', 220 | maxLines: 3, 221 | ), 222 | SizedBox(height: bigSpacing), 223 | 224 | // Repeat section 225 | const _FieldLabel(text: 'Repeat'), 226 | SizedBox(height: spacing), 227 | 228 | // Frequency row (Daily / Weekly / Monthly / No repeat) 229 | Wrap( 230 | spacing: 12, 231 | runSpacing: 12, 232 | children: [ 233 | _RepeatChip( 234 | label: 'Daily', 235 | selected: _repeatRule == 'Daily', 236 | onTap: () => _selectRepeatRule('Daily'), 237 | ), 238 | _RepeatChip( 239 | label: 'Weekly', 240 | selected: _repeatRule == 'Weekly', 241 | onTap: () => _selectRepeatRule('Weekly'), 242 | ), 243 | _RepeatChip( 244 | label: 'Monthly', 245 | selected: _repeatRule == 'Monthly', 246 | onTap: () => _selectRepeatRule('Monthly'), 247 | ), 248 | _RepeatChip( 249 | label: 'No repeat', 250 | selected: _repeatRule == null || _repeatRule == 'No repeat', 251 | onTap: () => _selectRepeatRule('No repeat'), 252 | ), 253 | ], 254 | ), 255 | SizedBox(height: spacing), 256 | 257 | // Weekday row (shown always; only applied when Weekly) 258 | // Order: Sunday..Saturday, with dark selected chips per rules 259 | Wrap( 260 | spacing: 12, 261 | runSpacing: 12, 262 | children: [ 263 | _WeekdayChip( 264 | label: 'Sunday', 265 | selected: _repeatWeekdays.contains(7), 266 | onTap: () => _toggleWeekday(7), 267 | ), 268 | _WeekdayChip( 269 | label: 'Monday', 270 | selected: _repeatWeekdays.contains(1), 271 | onTap: () => _toggleWeekday(1), 272 | ), 273 | _WeekdayChip( 274 | label: 'Tuesday', 275 | selected: _repeatWeekdays.contains(2), 276 | onTap: () => _toggleWeekday(2), 277 | ), 278 | _WeekdayChip( 279 | label: 'Wednesday', 280 | selected: _repeatWeekdays.contains(3), 281 | onTap: () => _toggleWeekday(3), 282 | ), 283 | _WeekdayChip( 284 | label: 'Thursday', 285 | selected: _repeatWeekdays.contains(4), 286 | onTap: () => _toggleWeekday(4), 287 | ), 288 | _WeekdayChip( 289 | label: 'Friday', 290 | selected: _repeatWeekdays.contains(5), 291 | onTap: () => _toggleWeekday(5), 292 | ), 293 | _WeekdayChip( 294 | label: 'Saturday', 295 | selected: _repeatWeekdays.contains(6), 296 | onTap: () => _toggleWeekday(6), 297 | ), 298 | ], 299 | ), 300 | SizedBox(height: bigSpacing), 301 | 302 | // Date & Time 303 | const _FieldLabel(text: 'Date & Time'), 304 | SizedBox(height: spacing), 305 | 306 | // Date field 307 | _PickerField( 308 | hint: 'Set date', 309 | valueText: _selectedDate != null ? _formatDate(_selectedDate!) : null, 310 | iconAsset: 'assets/calendar.svg', 311 | onTap: _pickDate, 312 | onClear: _selectedDate != null 313 | ? () => setState(() => _selectedDate = null) 314 | : null, 315 | ), 316 | SizedBox(height: spacing), 317 | 318 | // Time field 319 | _PickerField( 320 | hint: 'Set time', 321 | valueText: _selectedTime != null ? _formatTime(_selectedTime!) : null, 322 | iconAsset: 'assets/clock.svg', 323 | onTap: _pickTime, 324 | onClear: _selectedTime != null 325 | ? () => setState(() => _selectedTime = null) 326 | : null, 327 | ), 328 | 329 | // Bottom spacing 330 | SizedBox(height: width * 0.1), 331 | ], 332 | ), 333 | ), 334 | ), 335 | // Save button fixed to bottom visually via a large button in bottomNavigationBar 336 | bottomNavigationBar: Padding( 337 | padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), 338 | child: SafeArea( 339 | 340 | //minimum: _screenHPad.add(const EdgeInsets.only(bottom: 16)), 341 | child: SizedBox( 342 | height: 56, 343 | child: FilledButton( 344 | style: FilledButton.styleFrom( 345 | backgroundColor: const Color(0xFF3B82F6), // Blue 500 346 | shape: RoundedRectangleBorder( 347 | borderRadius: BorderRadius.circular(16), 348 | ), 349 | textStyle: const TextStyle( 350 | fontWeight: FontWeight.w700, 351 | fontSize: 16, 352 | ), 353 | ), 354 | onPressed: _save, 355 | child: const Text('Save'), 356 | ), 357 | ), 358 | ), 359 | ), 360 | ); 361 | } 362 | } 363 | 364 | /* ---------- Reusable widgets ---------- */ 365 | 366 | class _FieldLabel extends StatelessWidget { 367 | const _FieldLabel({required this.text}); 368 | final String text; 369 | 370 | @override 371 | Widget build(BuildContext context) { 372 | return Text( 373 | text, 374 | style: const TextStyle( 375 | color: Colors.black87, 376 | fontWeight: FontWeight.w700, 377 | fontSize: 14, 378 | ), 379 | ); 380 | } 381 | } 382 | 383 | class _InputField extends StatelessWidget { 384 | const _InputField({ 385 | required this.controller, 386 | required this.hint, 387 | this.maxLines = 1, 388 | this.textInputAction, 389 | }); 390 | 391 | final TextEditingController controller; 392 | final String hint; 393 | final int maxLines; 394 | final TextInputAction? textInputAction; 395 | 396 | @override 397 | Widget build(BuildContext context) { 398 | return TextField( 399 | controller: controller, 400 | textInputAction: textInputAction, 401 | maxLines: maxLines, 402 | decoration: InputDecoration( 403 | hintText: hint, 404 | contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), 405 | filled: true, 406 | fillColor: Colors.white, 407 | border: OutlineInputBorder( 408 | borderRadius: BorderRadius.circular(16), 409 | borderSide: BorderSide(color: Colors.grey.shade300), 410 | ), 411 | enabledBorder: OutlineInputBorder( 412 | borderRadius: BorderRadius.circular(16), 413 | borderSide: BorderSide(color: Colors.grey.shade300), 414 | ), 415 | focusedBorder: OutlineInputBorder( 416 | borderRadius: BorderRadius.circular(16), 417 | borderSide: const BorderSide(color: Color(0xFF2563EB), width: 2), 418 | ), 419 | ), 420 | style: const TextStyle(fontSize: 14, height: 1.4), 421 | ); 422 | } 423 | } 424 | 425 | class _ReminderButton extends StatelessWidget { 426 | const _ReminderButton({required this.enabled, required this.onTap}); 427 | final bool enabled; 428 | final VoidCallback onTap; 429 | 430 | @override 431 | Widget build(BuildContext context) { 432 | final bg = enabled ? _AddTaskPageState.blueColor : Colors.white; 433 | final fg = enabled ? Colors.white : Colors.black; 434 | 435 | return Align( 436 | alignment: Alignment.centerLeft, 437 | child: Material( 438 | color: bg, 439 | shape: StadiumBorder( 440 | side: BorderSide(color: Colors.grey.shade300), 441 | ), 442 | child: InkWell( 443 | onTap: onTap, 444 | customBorder: const StadiumBorder(), 445 | child: Padding( 446 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 447 | child: Row( 448 | mainAxisSize: MainAxisSize.min, 449 | children: [ 450 | Text( 451 | 'Set Reminder', 452 | style: TextStyle( 453 | color: fg, 454 | fontWeight: FontWeight.w700, 455 | ), 456 | ), 457 | const SizedBox(width: 8), 458 | SvgPicture.asset( 459 | enabled ? 'assets/bell_white.svg' : 'assets/bell.svg', 460 | height: 18, 461 | width: 18, 462 | colorFilter: enabled 463 | ? null 464 | : const ColorFilter.mode(Colors.black87, BlendMode.srcIn), 465 | ), 466 | ], 467 | ), 468 | ), 469 | ), 470 | ), 471 | ); 472 | } 473 | } 474 | 475 | class _RepeatChip extends StatelessWidget { 476 | const _RepeatChip({ 477 | required this.label, 478 | required this.selected, 479 | required this.onTap, 480 | }); 481 | 482 | final String label; 483 | final bool selected; 484 | final VoidCallback onTap; 485 | 486 | @override 487 | Widget build(BuildContext context) { 488 | final bg = selected ? Colors.black : Colors.white; 489 | final fg = selected ? Colors.white : Colors.black; 490 | 491 | return Material( 492 | color: bg, 493 | shape: StadiumBorder(side: BorderSide(color: Colors.grey.shade300)), 494 | child: InkWell( 495 | onTap: onTap, 496 | customBorder: const StadiumBorder(), 497 | child: Padding( 498 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 499 | child: Text( 500 | label, 501 | style: TextStyle( 502 | color: fg, 503 | fontWeight: FontWeight.w700, 504 | ), 505 | ), 506 | ), 507 | ), 508 | ); 509 | } 510 | } 511 | 512 | class _WeekdayChip extends StatelessWidget { 513 | const _WeekdayChip({ 514 | required this.label, 515 | required this.selected, 516 | required this.onTap, 517 | }); 518 | 519 | final String label; 520 | final bool selected; 521 | final VoidCallback onTap; 522 | 523 | @override 524 | Widget build(BuildContext context) { 525 | final bg = selected ? Colors.black : Colors.white; 526 | final fg = selected ? Colors.white : Colors.black; 527 | 528 | return Material( 529 | color: bg, 530 | shape: StadiumBorder(side: BorderSide(color: Colors.grey.shade300)), 531 | child: InkWell( 532 | onTap: onTap, 533 | customBorder: const StadiumBorder(), 534 | child: Padding( 535 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 536 | child: Text( 537 | label, 538 | style: TextStyle( 539 | color: fg, 540 | fontWeight: FontWeight.w700, 541 | ), 542 | ), 543 | ), 544 | ), 545 | ); 546 | } 547 | } 548 | 549 | class _PickerField extends StatelessWidget { 550 | const _PickerField({ 551 | required this.hint, 552 | required this.iconAsset, 553 | required this.onTap, 554 | this.valueText, 555 | this.onClear, 556 | }); 557 | 558 | final String hint; 559 | final String iconAsset; 560 | final String? valueText; 561 | final VoidCallback onTap; 562 | final VoidCallback? onClear; 563 | 564 | @override 565 | Widget build(BuildContext context) { 566 | final hasValue = valueText != null && valueText!.isNotEmpty; 567 | 568 | return Material( 569 | color: Colors.white, 570 | shape: RoundedRectangleBorder( 571 | borderRadius: BorderRadius.circular(16), 572 | side: BorderSide(color: Colors.grey.shade300), 573 | ), 574 | child: InkWell( 575 | borderRadius: BorderRadius.circular(16), 576 | onTap: onTap, 577 | child: Padding( 578 | padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), 579 | child: Row( 580 | children: [ 581 | SvgPicture.asset( 582 | iconAsset, 583 | height: 18, 584 | width: 18, 585 | colorFilter: 586 | const ColorFilter.mode(Colors.black87, BlendMode.srcIn), 587 | ), 588 | const SizedBox(width: 12), 589 | Expanded( 590 | child: Text( 591 | hasValue ? valueText! : hint, 592 | style: TextStyle( 593 | color: hasValue ? Colors.black : Colors.black54, 594 | fontWeight: hasValue ? FontWeight.w600 : FontWeight.w500, 595 | fontSize: 14, 596 | ), 597 | ), 598 | ), 599 | if (hasValue && onClear != null) 600 | IconButton( 601 | tooltip: 'Clear', 602 | icon: const Icon(Icons.close, size: 20, color: Colors.black54), 603 | onPressed: onClear, 604 | ), 605 | ], 606 | ), 607 | ), 608 | ), 609 | ); 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /lib/screens/edit_task_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_svg/flutter_svg.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'package:doable_todo_list_app/models/task_entity.dart'; 6 | import 'package:doable_todo_list_app/repositories/task_repository.dart'; 7 | import 'package:doable_todo_list_app/services/notification_service.dart'; 8 | 9 | // Import Task view model from Home if you keep it there, 10 | // or duplicate the minimal fields you need here. 11 | import 'package:doable_todo_list_app/screens/home_page.dart' show Task; 12 | 13 | class EditTaskPage extends StatefulWidget { 14 | const EditTaskPage({super.key}); 15 | 16 | @override 17 | State createState() => _EditTaskPageState(); 18 | } 19 | 20 | class _EditTaskPageState extends State { 21 | // Controllers 22 | final _titleCtrl = TextEditingController(); 23 | final _descCtrl = TextEditingController(); 24 | 25 | // Incoming task to edit 26 | late Task _task; 27 | 28 | // UI state 29 | bool _reminder = false; 30 | String? _repeatRule; // "Daily" | "Weekly" | "Monthly" | "No repeat" | null 31 | final Set _repeatWeekdays = {}; // 1=Mon ... 7=Sun 32 | DateTime? _selectedDate; 33 | TimeOfDay? _selectedTime; 34 | 35 | // Style constants 36 | static const Color blueColor = Color(0xFF2563EB); // button/active color 37 | 38 | // Convenience paddings 39 | EdgeInsets get _screenHPad { 40 | final w = MediaQuery.of(context).size.width; 41 | final hpad = (w * 0.05).clamp(16.0, 24.0); 42 | return EdgeInsets.symmetric(horizontal: hpad); 43 | } 44 | 45 | @override 46 | void initState() { 47 | super.initState(); 48 | // Read arguments after first frame to ensure context is mounted 49 | WidgetsBinding.instance.addPostFrameCallback((_) { 50 | final arg = ModalRoute.of(context)!.settings.arguments; 51 | _task = arg as Task; 52 | 53 | // Prefill text 54 | _titleCtrl.text = _task.title; 55 | _descCtrl.text = _task.description ?? ''; 56 | 57 | // Prefill toggles 58 | _reminder = _task.hasNotification; 59 | _repeatRule = _task.repeatRule; 60 | _hydrateWeekdaysFromRule(_repeatRule); 61 | 62 | // Prefill date/time (parse stored display strings) 63 | _selectedDate = _parseDateOrNull(_task.date); 64 | _selectedTime = _parseTimeOrNull(_task.time); 65 | 66 | setState(() {}); 67 | }); 68 | } 69 | 70 | @override 71 | void dispose() { 72 | _titleCtrl.dispose(); 73 | _descCtrl.dispose(); 74 | super.dispose(); 75 | } 76 | 77 | // ===== Formatting / parsing ===== 78 | 79 | String _formatDate(DateTime d) => DateFormat('dd/MM/yy').format(d); 80 | 81 | String _formatTime(TimeOfDay t) { 82 | final dt = DateTime(0, 1, 1, t.hour, t.minute); 83 | return DateFormat('h:mm a').format(dt); 84 | } 85 | 86 | DateTime? _parseDateOrNull(String? s) { 87 | if (s == null || s.trim().isEmpty) return null; 88 | try { 89 | return DateFormat('dd/MM/yy').parseStrict(s); 90 | } catch (_) { 91 | return null; 92 | } 93 | } 94 | 95 | TimeOfDay? _parseTimeOrNull(String? s) { 96 | if (s == null || s.trim().isEmpty) return null; 97 | try { 98 | final dt = DateFormat('h:mm a').parseStrict(s); 99 | return TimeOfDay.fromDateTime(dt); 100 | } catch (_) { 101 | return null; 102 | } 103 | } 104 | 105 | // ===== Repeat helpers (Weekly day parsing/selection) ===== 106 | 107 | void _hydrateWeekdaysFromRule(String? rule) { 108 | _repeatWeekdays.clear(); 109 | if (rule == null) return; 110 | if (!rule.startsWith('Weekly')) return; 111 | 112 | // Supports "Weekly:[1,2,3]" or "Weekly:1,2,3" or variants with spaces 113 | final exp = RegExp(r'(\d+)'); 114 | for (final m in exp.allMatches(rule)) { 115 | final v = int.tryParse(m.group(1)!); 116 | if (v != null && v >= 1 && v <= 7) _repeatWeekdays.add(v); 117 | } 118 | } 119 | 120 | void _selectRepeatRule(String rule) { 121 | setState(() { 122 | _repeatRule = rule; 123 | if (rule != 'Weekly') { 124 | _repeatWeekdays.clear(); 125 | } 126 | }); 127 | } 128 | 129 | void _toggleWeekday(int weekday) { 130 | setState(() { 131 | if (_repeatWeekdays.contains(weekday)) { 132 | _repeatWeekdays.remove(weekday); 133 | } else { 134 | _repeatWeekdays.add(weekday); 135 | } 136 | if (_repeatWeekdays.isNotEmpty) { 137 | _repeatRule = 'Weekly'; // force Weekly if any day is picked 138 | } 139 | }); 140 | } 141 | 142 | // ===== Pickers ===== 143 | 144 | Future _pickDate() async { 145 | final now = DateTime.now(); 146 | final picked = await showDatePicker( 147 | context: context, 148 | initialDate: _selectedDate ?? now, 149 | firstDate: DateTime(now.year - 1), 150 | lastDate: DateTime(now.year + 5), 151 | helpText: 'Select date', 152 | ); 153 | if (picked != null) setState(() => _selectedDate = picked); 154 | } 155 | 156 | Future _pickTime() async { 157 | final picked = await showTimePicker( 158 | context: context, 159 | initialTime: _selectedTime ?? TimeOfDay.now(), 160 | helpText: 'Select time', 161 | ); 162 | if (picked != null) setState(() => _selectedTime = picked); 163 | } 164 | 165 | void _toggleReminder() async { 166 | final userEnabled = await NotificationService.areNotificationsEnabledByUser(); 167 | 168 | if (!userEnabled) { 169 | if (!mounted) return; 170 | ScaffoldMessenger.of(context).showSnackBar( 171 | SnackBar( 172 | content: const Text('Notifications are disabled in settings'), 173 | action: SnackBarAction( 174 | label: 'Settings', 175 | onPressed: () { 176 | Navigator.pushNamed(context, 'settings'); 177 | }, 178 | ), 179 | ), 180 | ); 181 | return; 182 | } 183 | 184 | setState(() => _reminder = !_reminder); 185 | } 186 | 187 | // ===== Save ===== 188 | 189 | Future _save() async { 190 | final title = _titleCtrl.text.trim(); 191 | if (title.isEmpty) { 192 | ScaffoldMessenger.of(context).showSnackBar( 193 | const SnackBar(content: Text('Please enter a title')), 194 | ); 195 | return; 196 | } 197 | 198 | // Build display strings 199 | final dateStr = _selectedDate != null ? _formatDate(_selectedDate!) : null; 200 | final timeStr = _selectedTime != null ? _formatTime(_selectedTime!) : null; 201 | 202 | // Build repeat rule string 203 | String? normalizedRepeat; 204 | if (_repeatRule == null || _repeatRule == 'No repeat') { 205 | normalizedRepeat = null; 206 | } else if (_repeatRule == 'Weekly' && _repeatWeekdays.isNotEmpty) { 207 | final list = _repeatWeekdays.toList()..sort(); // 1..7 stable order 208 | normalizedRepeat = 'Weekly:${list.toString()}'; // Weekly:[1,2,4] 209 | } else { 210 | normalizedRepeat = _repeatRule; 211 | } 212 | 213 | final entity = TaskEntity( 214 | id: _task.id, // required for update 215 | title: title, 216 | description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(), 217 | time: timeStr, 218 | date: dateStr, 219 | hasNotification: _reminder, 220 | repeatRule: normalizedRepeat, 221 | completed: _task.completed, // preserve current completed state 222 | ); 223 | 224 | await TaskRepository().update(entity); 225 | 226 | if (mounted) Navigator.pop(context, true); // signal Home to refresh 227 | } 228 | 229 | @override 230 | Widget build(BuildContext context) { 231 | // Simple guard for initial frame before arguments arrive 232 | if (!(_titleCtrl.text.isNotEmpty || ModalRoute.of(context)?.settings.arguments != null)) { 233 | return const Scaffold(body: Center(child: CircularProgressIndicator())); 234 | } 235 | 236 | final spacing = 16.0; 237 | final bigSpacing = 24.0; 238 | 239 | return Scaffold( 240 | backgroundColor: Colors.white, 241 | appBar: AppBar( 242 | backgroundColor: Colors.white, 243 | elevation: 0, 244 | surfaceTintColor: Colors.white, 245 | leading: IconButton( 246 | onPressed: () => Navigator.pop(context, false), 247 | icon: const Icon(Icons.arrow_back, color: Colors.black), 248 | tooltip: 'Back', 249 | ), 250 | title: const Text( 251 | 'Modify to-do', 252 | style: TextStyle( 253 | fontSize: 24, 254 | fontWeight: FontWeight.w800, 255 | color: Colors.black, 256 | ), 257 | ), 258 | centerTitle: false, 259 | ), 260 | body: SafeArea( 261 | child: SingleChildScrollView( 262 | padding: _screenHPad.add(const EdgeInsets.only(bottom: 24, top: 8)), 263 | child: Column( 264 | crossAxisAlignment: CrossAxisAlignment.start, 265 | children: [ 266 | // Reminder button: blue bg + white icon/text when enabled 267 | _ReminderButton( 268 | enabled: _reminder, 269 | onTap: _toggleReminder, 270 | ), 271 | SizedBox(height: bigSpacing), 272 | 273 | const _FieldLabel(text: 'Tell us about your task'), 274 | SizedBox(height: spacing), 275 | 276 | _InputField( 277 | controller: _titleCtrl, 278 | hint: 'Title', 279 | textInputAction: TextInputAction.next, 280 | ), 281 | SizedBox(height: spacing), 282 | _InputField( 283 | controller: _descCtrl, 284 | hint: 'Description', 285 | maxLines: 3, 286 | ), 287 | SizedBox(height: bigSpacing), 288 | 289 | const _FieldLabel(text: 'Repeat'), 290 | SizedBox(height: spacing), 291 | 292 | // Frequency chips 293 | Wrap( 294 | spacing: 12, 295 | runSpacing: 12, 296 | children: [ 297 | _RepeatChip( 298 | label: 'Daily', 299 | selected: _repeatRule == 'Daily', 300 | onTap: () => _selectRepeatRule('Daily'), 301 | ), 302 | _RepeatChip( 303 | label: 'Weekly', 304 | selected: _repeatRule == 'Weekly', 305 | onTap: () => _selectRepeatRule('Weekly'), 306 | ), 307 | _RepeatChip( 308 | label: 'Monthly', 309 | selected: _repeatRule == 'Monthly', 310 | onTap: () => _selectRepeatRule('Monthly'), 311 | ), 312 | _RepeatChip( 313 | label: 'No repeat', 314 | selected: _repeatRule == null || _repeatRule == 'No repeat', 315 | onTap: () => _selectRepeatRule('No repeat'), 316 | ), 317 | ], 318 | ), 319 | const SizedBox(height: 12), 320 | 321 | // Weekday chips (always visible; applied when Weekly) 322 | Wrap( 323 | spacing: 12, 324 | runSpacing: 12, 325 | children: [ 326 | _WeekdayChip( 327 | label: 'Sunday', 328 | selected: _repeatWeekdays.contains(7), 329 | onTap: () => _toggleWeekday(7), 330 | ), 331 | _WeekdayChip( 332 | label: 'Monday', 333 | selected: _repeatWeekdays.contains(1), 334 | onTap: () => _toggleWeekday(1), 335 | ), 336 | _WeekdayChip( 337 | label: 'Tuesday', 338 | selected: _repeatWeekdays.contains(2), 339 | onTap: () => _toggleWeekday(2), 340 | ), 341 | _WeekdayChip( 342 | label: 'Wednesday', 343 | selected: _repeatWeekdays.contains(3), 344 | onTap: () => _toggleWeekday(3), 345 | ), 346 | _WeekdayChip( 347 | label: 'Thursday', 348 | selected: _repeatWeekdays.contains(4), 349 | onTap: () => _toggleWeekday(4), 350 | ), 351 | _WeekdayChip( 352 | label: 'Friday', 353 | selected: _repeatWeekdays.contains(5), 354 | onTap: () => _toggleWeekday(5), 355 | ), 356 | _WeekdayChip( 357 | label: 'Saturday', 358 | selected: _repeatWeekdays.contains(6), 359 | onTap: () => _toggleWeekday(6), 360 | ), 361 | ], 362 | ), 363 | SizedBox(height: bigSpacing), 364 | 365 | const _FieldLabel(text: 'Date & Time'), 366 | SizedBox(height: spacing), 367 | 368 | // Date field with calendar icon 369 | _PickerField( 370 | hint: 'Set date', 371 | valueText: _selectedDate != null ? _formatDate(_selectedDate!) : null, 372 | iconAsset: 'assets/calendar.svg', 373 | onTap: _pickDate, 374 | onClear: _selectedDate != null 375 | ? () => setState(() => _selectedDate = null) 376 | : null, 377 | ), 378 | SizedBox(height: spacing), 379 | 380 | // Time field with clock icon 381 | _PickerField( 382 | hint: 'Set time', 383 | valueText: _selectedTime != null ? _formatTime(_selectedTime!) : null, 384 | iconAsset: 'assets/clock.svg', 385 | onTap: _pickTime, 386 | onClear: _selectedTime != null 387 | ? () => setState(() => _selectedTime = null) 388 | : null, 389 | ), 390 | const SizedBox(height: 8), 391 | ], 392 | ), 393 | ), 394 | ), 395 | 396 | // Bottom Save button with extra bottom padding (and safe area) 397 | bottomNavigationBar: Padding( 398 | padding: const EdgeInsets.fromLTRB(16, 0, 16, 32), 399 | child: SafeArea( 400 | top: false, 401 | child: SizedBox( 402 | height: 56, 403 | child: FilledButton( 404 | style: FilledButton.styleFrom( 405 | backgroundColor: const Color(0xFF3B82F6), 406 | shape: RoundedRectangleBorder( 407 | borderRadius: BorderRadius.circular(16), 408 | ), 409 | textStyle: const TextStyle( 410 | fontWeight: FontWeight.w700, 411 | fontSize: 16, 412 | ), 413 | ), 414 | onPressed: _save, 415 | child: const Text('Save'), 416 | ), 417 | ), 418 | ), 419 | ), 420 | ); 421 | } 422 | } 423 | 424 | /* ================= Reusable widgets ================= */ 425 | 426 | class _FieldLabel extends StatelessWidget { 427 | const _FieldLabel({required this.text}); 428 | final String text; 429 | @override 430 | Widget build(BuildContext context) { 431 | return Text( 432 | text, 433 | style: const TextStyle( 434 | color: Colors.black87, 435 | fontWeight: FontWeight.w700, 436 | fontSize: 14, 437 | ), 438 | ); 439 | } 440 | } 441 | 442 | class _InputField extends StatelessWidget { 443 | const _InputField({ 444 | required this.controller, 445 | required this.hint, 446 | this.maxLines = 1, 447 | this.textInputAction, 448 | }); 449 | 450 | final TextEditingController controller; 451 | final String hint; 452 | final int maxLines; 453 | final TextInputAction? textInputAction; 454 | 455 | @override 456 | Widget build(BuildContext context) { 457 | return TextField( 458 | controller: controller, 459 | textInputAction: textInputAction, 460 | maxLines: maxLines, 461 | decoration: InputDecoration( 462 | hintText: hint, 463 | contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), 464 | filled: true, 465 | fillColor: Colors.white, 466 | border: OutlineInputBorder( 467 | borderRadius: BorderRadius.circular(16), 468 | borderSide: BorderSide(color: Colors.grey.shade300), 469 | ), 470 | enabledBorder: OutlineInputBorder( 471 | borderRadius: BorderRadius.circular(16), 472 | borderSide: BorderSide(color: Colors.grey.shade300), 473 | ), 474 | focusedBorder: OutlineInputBorder( 475 | borderRadius: BorderRadius.circular(16), 476 | borderSide: const BorderSide(color: Color(0xFF2563EB), width: 2), 477 | ), 478 | ), 479 | style: const TextStyle(fontSize: 14, height: 1.4), 480 | ); 481 | } 482 | } 483 | 484 | class _ReminderButton extends StatelessWidget { 485 | const _ReminderButton({required this.enabled, required this.onTap}); 486 | final bool enabled; 487 | final VoidCallback onTap; 488 | 489 | @override 490 | Widget build(BuildContext context) { 491 | final bg = enabled ? _EditTaskPageState.blueColor : Colors.white; 492 | final fg = enabled ? Colors.white : Colors.black; 493 | 494 | return Align( 495 | alignment: Alignment.centerLeft, 496 | child: Material( 497 | color: bg, 498 | shape: StadiumBorder( 499 | side: BorderSide(color: Colors.grey.shade300), 500 | ), 501 | child: InkWell( 502 | onTap: onTap, 503 | customBorder: const StadiumBorder(), 504 | child: Padding( 505 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 506 | child: Row( 507 | mainAxisSize: MainAxisSize.min, 508 | children: [ 509 | Text( 510 | 'Set Reminder', 511 | style: TextStyle( 512 | color: fg, 513 | fontWeight: FontWeight.w700, 514 | ), 515 | ), 516 | const SizedBox(width: 8), 517 | SvgPicture.asset( 518 | enabled ? 'assets/bell_white.svg' : 'assets/bell.svg', 519 | height: 18, 520 | width: 18, 521 | colorFilter: enabled 522 | ? null 523 | : const ColorFilter.mode(Colors.black87, BlendMode.srcIn), 524 | ), 525 | ], 526 | ), 527 | ), 528 | ), 529 | ), 530 | ); 531 | } 532 | } 533 | 534 | class _RepeatChip extends StatelessWidget { 535 | const _RepeatChip({ 536 | required this.label, 537 | required this.selected, 538 | required this.onTap, 539 | }); 540 | 541 | final String label; 542 | final bool selected; 543 | final VoidCallback onTap; 544 | 545 | @override 546 | Widget build(BuildContext context) { 547 | final bg = selected ? Colors.black : Colors.white; 548 | final fg = selected ? Colors.white : Colors.black; 549 | 550 | return Material( 551 | color: bg, 552 | shape: StadiumBorder(side: BorderSide(color: Colors.grey.shade300)), 553 | child: InkWell( 554 | onTap: onTap, 555 | customBorder: const StadiumBorder(), 556 | child: Padding( 557 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 558 | child: Text( 559 | label, 560 | style: TextStyle( 561 | color: fg, 562 | fontWeight: FontWeight.w700, 563 | ), 564 | ), 565 | ), 566 | ), 567 | ); 568 | } 569 | } 570 | 571 | class _WeekdayChip extends StatelessWidget { 572 | const _WeekdayChip({ 573 | required this.label, 574 | required this.selected, 575 | required this.onTap, 576 | }); 577 | 578 | final String label; 579 | final bool selected; 580 | final VoidCallback onTap; 581 | 582 | @override 583 | Widget build(BuildContext context) { 584 | final bg = selected ? Colors.black : Colors.white; 585 | final fg = selected ? Colors.white : Colors.black; 586 | 587 | return Material( 588 | color: bg, 589 | shape: StadiumBorder(side: BorderSide(color: Colors.grey.shade300)), 590 | child: InkWell( 591 | onTap: onTap, 592 | customBorder: const StadiumBorder(), 593 | child: Padding( 594 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), 595 | child: Text( 596 | label, 597 | style: TextStyle( 598 | color: fg, 599 | fontWeight: FontWeight.w700, 600 | ), 601 | ), 602 | ), 603 | ), 604 | ); 605 | } 606 | } 607 | 608 | class _PickerField extends StatelessWidget { 609 | const _PickerField({ 610 | required this.hint, 611 | required this.iconAsset, 612 | required this.onTap, 613 | this.valueText, 614 | this.onClear, 615 | }); 616 | 617 | final String hint; 618 | final String iconAsset; 619 | final String? valueText; 620 | final VoidCallback onTap; 621 | final VoidCallback? onClear; 622 | 623 | @override 624 | Widget build(BuildContext context) { 625 | final hasValue = valueText != null && valueText!.isNotEmpty; 626 | 627 | return Material( 628 | color: Colors.white, 629 | shape: RoundedRectangleBorder( 630 | borderRadius: BorderRadius.circular(16), 631 | side: BorderSide(color: Colors.grey.shade300), 632 | ), 633 | child: InkWell( 634 | borderRadius: BorderRadius.circular(16), 635 | onTap: onTap, 636 | child: Padding( 637 | padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), 638 | child: Row( 639 | children: [ 640 | SvgPicture.asset( 641 | iconAsset, 642 | height: 18, 643 | width: 18, 644 | colorFilter: 645 | const ColorFilter.mode(Colors.black87, BlendMode.srcIn), 646 | ), 647 | const SizedBox(width: 12), 648 | Expanded( 649 | child: Text( 650 | hasValue ? valueText! : hint, 651 | style: TextStyle( 652 | color: hasValue ? Colors.black : Colors.black54, 653 | fontWeight: hasValue ? FontWeight.w600 : FontWeight.w500, 654 | fontSize: 14, 655 | ), 656 | ), 657 | ), 658 | if (hasValue && onClear != null) 659 | IconButton( 660 | tooltip: 'Clear', 661 | icon: const Icon(Icons.close, size: 20, color: Colors.black54), 662 | onPressed: onClear, 663 | ), 664 | ], 665 | ), 666 | ), 667 | ), 668 | ); 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 97C146E61CF9000F007C117D /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = 97C146ED1CF9000F007C117D; 25 | remoteInfo = Runner; 26 | }; 27 | /* End PBXContainerItemProxy section */ 28 | 29 | /* Begin PBXCopyFilesBuildPhase section */ 30 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 31 | isa = PBXCopyFilesBuildPhase; 32 | buildActionMask = 2147483647; 33 | dstPath = ""; 34 | dstSubfolderSpec = 10; 35 | files = ( 36 | ); 37 | name = "Embed Frameworks"; 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXCopyFilesBuildPhase section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 44 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 45 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 46 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 47 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 48 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 49 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 50 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 51 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 53 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 54 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 57 | 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 9740EEB11CF90186004384FC /* Flutter */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 75 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 76 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 77 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 78 | ); 79 | name = Flutter; 80 | sourceTree = ""; 81 | }; 82 | 331C8082294A63A400263BE5 /* RunnerTests */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 331C807B294A618700263BE5 /* RunnerTests.swift */, 86 | ); 87 | path = RunnerTests; 88 | sourceTree = ""; 89 | }; 90 | 97C146E51CF9000F007C117D = { 91 | isa = PBXGroup; 92 | children = ( 93 | 9740EEB11CF90186004384FC /* Flutter */, 94 | 97C146F01CF9000F007C117D /* Runner */, 95 | 97C146EF1CF9000F007C117D /* Products */, 96 | 331C8082294A63A400263BE5 /* RunnerTests */, 97 | ); 98 | sourceTree = ""; 99 | }; 100 | 97C146EF1CF9000F007C117D /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 97C146EE1CF9000F007C117D /* Runner.app */, 104 | 331C8081294A63A400263BE5 /* RunnerTests.xctest */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 97C146F01CF9000F007C117D /* Runner */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 113 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 114 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 115 | 97C147021CF9000F007C117D /* Info.plist */, 116 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 117 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 118 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 119 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 120 | ); 121 | path = Runner; 122 | sourceTree = ""; 123 | }; 124 | /* End PBXGroup section */ 125 | 126 | /* Begin PBXNativeTarget section */ 127 | 331C8080294A63A400263BE5 /* RunnerTests */ = { 128 | isa = PBXNativeTarget; 129 | buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; 130 | buildPhases = ( 131 | 331C807D294A63A400263BE5 /* Sources */, 132 | 331C807E294A63A400263BE5 /* Frameworks */, 133 | 331C807F294A63A400263BE5 /* Resources */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | 331C8086294A63A400263BE5 /* PBXTargetDependency */, 139 | ); 140 | name = RunnerTests; 141 | productName = RunnerTests; 142 | productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; 143 | productType = "com.apple.product-type.bundle.unit-test"; 144 | }; 145 | 97C146ED1CF9000F007C117D /* Runner */ = { 146 | isa = PBXNativeTarget; 147 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 148 | buildPhases = ( 149 | 9740EEB61CF901F6004384FC /* Run Script */, 150 | 97C146EA1CF9000F007C117D /* Sources */, 151 | 97C146EB1CF9000F007C117D /* Frameworks */, 152 | 97C146EC1CF9000F007C117D /* Resources */, 153 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 154 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | ); 160 | name = Runner; 161 | productName = Runner; 162 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 163 | productType = "com.apple.product-type.application"; 164 | }; 165 | /* End PBXNativeTarget section */ 166 | 167 | /* Begin PBXProject section */ 168 | 97C146E61CF9000F007C117D /* Project object */ = { 169 | isa = PBXProject; 170 | attributes = { 171 | BuildIndependentTargetsInParallel = YES; 172 | LastUpgradeCheck = 1430; 173 | ORGANIZATIONNAME = ""; 174 | TargetAttributes = { 175 | 331C8080294A63A400263BE5 = { 176 | CreatedOnToolsVersion = 14.0; 177 | TestTargetID = 97C146ED1CF9000F007C117D; 178 | }; 179 | 97C146ED1CF9000F007C117D = { 180 | CreatedOnToolsVersion = 7.3.1; 181 | LastSwiftMigration = 1100; 182 | }; 183 | }; 184 | }; 185 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 186 | compatibilityVersion = "Xcode 9.3"; 187 | developmentRegion = en; 188 | hasScannedForEncodings = 0; 189 | knownRegions = ( 190 | en, 191 | Base, 192 | ); 193 | mainGroup = 97C146E51CF9000F007C117D; 194 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 195 | projectDirPath = ""; 196 | projectRoot = ""; 197 | targets = ( 198 | 97C146ED1CF9000F007C117D /* Runner */, 199 | 331C8080294A63A400263BE5 /* RunnerTests */, 200 | ); 201 | }; 202 | /* End PBXProject section */ 203 | 204 | /* Begin PBXResourcesBuildPhase section */ 205 | 331C807F294A63A400263BE5 /* Resources */ = { 206 | isa = PBXResourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | 97C146EC1CF9000F007C117D /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 217 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 218 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 219 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXResourcesBuildPhase section */ 224 | 225 | /* Begin PBXShellScriptBuildPhase section */ 226 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 227 | isa = PBXShellScriptBuildPhase; 228 | alwaysOutOfDate = 1; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | ); 232 | inputPaths = ( 233 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 234 | ); 235 | name = "Thin Binary"; 236 | outputPaths = ( 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | shellPath = /bin/sh; 240 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 241 | }; 242 | 9740EEB61CF901F6004384FC /* Run Script */ = { 243 | isa = PBXShellScriptBuildPhase; 244 | alwaysOutOfDate = 1; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | ); 248 | inputPaths = ( 249 | ); 250 | name = "Run Script"; 251 | outputPaths = ( 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | shellPath = /bin/sh; 255 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 256 | }; 257 | /* End PBXShellScriptBuildPhase section */ 258 | 259 | /* Begin PBXSourcesBuildPhase section */ 260 | 331C807D294A63A400263BE5 /* Sources */ = { 261 | isa = PBXSourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | 97C146EA1CF9000F007C117D /* Sources */ = { 269 | isa = PBXSourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 273 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXSourcesBuildPhase section */ 278 | 279 | /* Begin PBXTargetDependency section */ 280 | 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { 281 | isa = PBXTargetDependency; 282 | target = 97C146ED1CF9000F007C117D /* Runner */; 283 | targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; 284 | }; 285 | /* End PBXTargetDependency section */ 286 | 287 | /* Begin PBXVariantGroup section */ 288 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 289 | isa = PBXVariantGroup; 290 | children = ( 291 | 97C146FB1CF9000F007C117D /* Base */, 292 | ); 293 | name = Main.storyboard; 294 | sourceTree = ""; 295 | }; 296 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | 97C147001CF9000F007C117D /* Base */, 300 | ); 301 | name = LaunchScreen.storyboard; 302 | sourceTree = ""; 303 | }; 304 | /* End PBXVariantGroup section */ 305 | 306 | /* Begin XCBuildConfiguration section */ 307 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_EMPTY_BODY = YES; 323 | CLANG_WARN_ENUM_CONVERSION = YES; 324 | CLANG_WARN_INFINITE_RECURSION = YES; 325 | CLANG_WARN_INT_CONVERSION = YES; 326 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 328 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 330 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 331 | CLANG_WARN_STRICT_PROTOTYPES = YES; 332 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 336 | COPY_PHASE_STRIP = NO; 337 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 338 | ENABLE_NS_ASSERTIONS = NO; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu99; 341 | GCC_NO_COMMON_BLOCKS = YES; 342 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 343 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 344 | GCC_WARN_UNDECLARED_SELECTOR = YES; 345 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 346 | GCC_WARN_UNUSED_FUNCTION = YES; 347 | GCC_WARN_UNUSED_VARIABLE = YES; 348 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 349 | MTL_ENABLE_DEBUG_INFO = NO; 350 | SDKROOT = iphoneos; 351 | SUPPORTED_PLATFORMS = iphoneos; 352 | TARGETED_DEVICE_FAMILY = "1,2"; 353 | VALIDATE_PRODUCT = YES; 354 | }; 355 | name = Profile; 356 | }; 357 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 358 | isa = XCBuildConfiguration; 359 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 360 | buildSettings = { 361 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 362 | CLANG_ENABLE_MODULES = YES; 363 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 364 | ENABLE_BITCODE = NO; 365 | INFOPLIST_FILE = Runner/Info.plist; 366 | LD_RUNPATH_SEARCH_PATHS = ( 367 | "$(inherited)", 368 | "@executable_path/Frameworks", 369 | ); 370 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 373 | SWIFT_VERSION = 5.0; 374 | VERSIONING_SYSTEM = "apple-generic"; 375 | }; 376 | name = Profile; 377 | }; 378 | 331C8088294A63A400263BE5 /* Debug */ = { 379 | isa = XCBuildConfiguration; 380 | baseConfigurationReference = AE0B7B92F70575B8D7E0D07E /* Pods-RunnerTests.debug.xcconfig */; 381 | buildSettings = { 382 | BUNDLE_LOADER = "$(TEST_HOST)"; 383 | CODE_SIGN_STYLE = Automatic; 384 | CURRENT_PROJECT_VERSION = 1; 385 | GENERATE_INFOPLIST_FILE = YES; 386 | MARKETING_VERSION = 1.0; 387 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp.RunnerTests; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 390 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 391 | SWIFT_VERSION = 5.0; 392 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 393 | }; 394 | name = Debug; 395 | }; 396 | 331C8089294A63A400263BE5 /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | baseConfigurationReference = 89B67EB44CE7B6631473024E /* Pods-RunnerTests.release.xcconfig */; 399 | buildSettings = { 400 | BUNDLE_LOADER = "$(TEST_HOST)"; 401 | CODE_SIGN_STYLE = Automatic; 402 | CURRENT_PROJECT_VERSION = 1; 403 | GENERATE_INFOPLIST_FILE = YES; 404 | MARKETING_VERSION = 1.0; 405 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp.RunnerTests; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SWIFT_VERSION = 5.0; 408 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 409 | }; 410 | name = Release; 411 | }; 412 | 331C808A294A63A400263BE5 /* Profile */ = { 413 | isa = XCBuildConfiguration; 414 | baseConfigurationReference = 640959BDD8F10B91D80A66BE /* Pods-RunnerTests.profile.xcconfig */; 415 | buildSettings = { 416 | BUNDLE_LOADER = "$(TEST_HOST)"; 417 | CODE_SIGN_STYLE = Automatic; 418 | CURRENT_PROJECT_VERSION = 1; 419 | GENERATE_INFOPLIST_FILE = YES; 420 | MARKETING_VERSION = 1.0; 421 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp.RunnerTests; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | SWIFT_VERSION = 5.0; 424 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; 425 | }; 426 | name = Profile; 427 | }; 428 | 97C147031CF9000F007C117D /* Debug */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ALWAYS_SEARCH_USER_PATHS = NO; 432 | CLANG_ANALYZER_NONNULL = YES; 433 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 434 | CLANG_CXX_LIBRARY = "libc++"; 435 | CLANG_ENABLE_MODULES = YES; 436 | CLANG_ENABLE_OBJC_ARC = YES; 437 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 438 | CLANG_WARN_BOOL_CONVERSION = YES; 439 | CLANG_WARN_COMMA = YES; 440 | CLANG_WARN_CONSTANT_CONVERSION = YES; 441 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 442 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 443 | CLANG_WARN_EMPTY_BODY = YES; 444 | CLANG_WARN_ENUM_CONVERSION = YES; 445 | CLANG_WARN_INFINITE_RECURSION = YES; 446 | CLANG_WARN_INT_CONVERSION = YES; 447 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 448 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 449 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 451 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 452 | CLANG_WARN_STRICT_PROTOTYPES = YES; 453 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 454 | CLANG_WARN_UNREACHABLE_CODE = YES; 455 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 456 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 457 | COPY_PHASE_STRIP = NO; 458 | DEBUG_INFORMATION_FORMAT = dwarf; 459 | ENABLE_STRICT_OBJC_MSGSEND = YES; 460 | ENABLE_TESTABILITY = YES; 461 | GCC_C_LANGUAGE_STANDARD = gnu99; 462 | GCC_DYNAMIC_NO_PIC = NO; 463 | GCC_NO_COMMON_BLOCKS = YES; 464 | GCC_OPTIMIZATION_LEVEL = 0; 465 | GCC_PREPROCESSOR_DEFINITIONS = ( 466 | "DEBUG=1", 467 | "$(inherited)", 468 | ); 469 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 470 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 471 | GCC_WARN_UNDECLARED_SELECTOR = YES; 472 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 473 | GCC_WARN_UNUSED_FUNCTION = YES; 474 | GCC_WARN_UNUSED_VARIABLE = YES; 475 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 476 | MTL_ENABLE_DEBUG_INFO = YES; 477 | ONLY_ACTIVE_ARCH = YES; 478 | SDKROOT = iphoneos; 479 | TARGETED_DEVICE_FAMILY = "1,2"; 480 | }; 481 | name = Debug; 482 | }; 483 | 97C147041CF9000F007C117D /* Release */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | ALWAYS_SEARCH_USER_PATHS = NO; 487 | CLANG_ANALYZER_NONNULL = YES; 488 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 489 | CLANG_CXX_LIBRARY = "libc++"; 490 | CLANG_ENABLE_MODULES = YES; 491 | CLANG_ENABLE_OBJC_ARC = YES; 492 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 493 | CLANG_WARN_BOOL_CONVERSION = YES; 494 | CLANG_WARN_COMMA = YES; 495 | CLANG_WARN_CONSTANT_CONVERSION = YES; 496 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 497 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 498 | CLANG_WARN_EMPTY_BODY = YES; 499 | CLANG_WARN_ENUM_CONVERSION = YES; 500 | CLANG_WARN_INFINITE_RECURSION = YES; 501 | CLANG_WARN_INT_CONVERSION = YES; 502 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 503 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 504 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 505 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 506 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 507 | CLANG_WARN_STRICT_PROTOTYPES = YES; 508 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 509 | CLANG_WARN_UNREACHABLE_CODE = YES; 510 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 511 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 512 | COPY_PHASE_STRIP = NO; 513 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 514 | ENABLE_NS_ASSERTIONS = NO; 515 | ENABLE_STRICT_OBJC_MSGSEND = YES; 516 | GCC_C_LANGUAGE_STANDARD = gnu99; 517 | GCC_NO_COMMON_BLOCKS = YES; 518 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 519 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 520 | GCC_WARN_UNDECLARED_SELECTOR = YES; 521 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 522 | GCC_WARN_UNUSED_FUNCTION = YES; 523 | GCC_WARN_UNUSED_VARIABLE = YES; 524 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 525 | MTL_ENABLE_DEBUG_INFO = NO; 526 | SDKROOT = iphoneos; 527 | SUPPORTED_PLATFORMS = iphoneos; 528 | SWIFT_COMPILATION_MODE = wholemodule; 529 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 530 | TARGETED_DEVICE_FAMILY = "1,2"; 531 | VALIDATE_PRODUCT = YES; 532 | }; 533 | name = Release; 534 | }; 535 | 97C147061CF9000F007C117D /* Debug */ = { 536 | isa = XCBuildConfiguration; 537 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 538 | buildSettings = { 539 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 540 | CLANG_ENABLE_MODULES = YES; 541 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 542 | ENABLE_BITCODE = NO; 543 | INFOPLIST_FILE = Runner/Info.plist; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/Frameworks", 547 | ); 548 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp; 549 | PRODUCT_NAME = "$(TARGET_NAME)"; 550 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 551 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 552 | SWIFT_VERSION = 5.0; 553 | VERSIONING_SYSTEM = "apple-generic"; 554 | }; 555 | name = Debug; 556 | }; 557 | 97C147071CF9000F007C117D /* Release */ = { 558 | isa = XCBuildConfiguration; 559 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 560 | buildSettings = { 561 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 562 | CLANG_ENABLE_MODULES = YES; 563 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 564 | ENABLE_BITCODE = NO; 565 | INFOPLIST_FILE = Runner/Info.plist; 566 | LD_RUNPATH_SEARCH_PATHS = ( 567 | "$(inherited)", 568 | "@executable_path/Frameworks", 569 | ); 570 | PRODUCT_BUNDLE_IDENTIFIER = com.example.doableTodoListApp; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 573 | SWIFT_VERSION = 5.0; 574 | VERSIONING_SYSTEM = "apple-generic"; 575 | }; 576 | name = Release; 577 | }; 578 | /* End XCBuildConfiguration section */ 579 | 580 | /* Begin XCConfigurationList section */ 581 | 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 331C8088294A63A400263BE5 /* Debug */, 585 | 331C8089294A63A400263BE5 /* Release */, 586 | 331C808A294A63A400263BE5 /* Profile */, 587 | ); 588 | defaultConfigurationIsVisible = 0; 589 | defaultConfigurationName = Release; 590 | }; 591 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 592 | isa = XCConfigurationList; 593 | buildConfigurations = ( 594 | 97C147031CF9000F007C117D /* Debug */, 595 | 97C147041CF9000F007C117D /* Release */, 596 | 249021D3217E4FDB00AE95B9 /* Profile */, 597 | ); 598 | defaultConfigurationIsVisible = 0; 599 | defaultConfigurationName = Release; 600 | }; 601 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 602 | isa = XCConfigurationList; 603 | buildConfigurations = ( 604 | 97C147061CF9000F007C117D /* Debug */, 605 | 97C147071CF9000F007C117D /* Release */, 606 | 249021D4217E4FDB00AE95B9 /* Profile */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | /* End XCConfigurationList section */ 612 | }; 613 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 614 | } 615 | --------------------------------------------------------------------------------