├── test └── widget_test.dart ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 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-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── LaunchBackground.imageset │ │ │ ├── background.png │ │ │ ├── darkbackground.png │ │ │ └── Contents.json │ │ └── BrandingImage.imageset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ └── Main.storyboard │ └── Info.plist ├── Runner.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── lib ├── models │ ├── sync_status.dart │ ├── sync_item_error.dart │ ├── dynamic_object.dart │ ├── sliding_page.dart │ ├── either.dart │ ├── serializers │ │ ├── color_serializer.dart │ │ ├── locale_serializer.dart │ │ └── datetime_serializer.dart │ ├── task_filter.dart │ ├── user.dart │ ├── auth_credentials.g.dart │ ├── geo_location.dart │ ├── bottom_navigation_bar_item.dart │ ├── user.g.dart │ ├── notification_data.g.dart │ ├── geo_location.g.dart │ ├── category.g.dart │ ├── active_session.g.dart │ ├── task.g.dart │ ├── notification_type.dart │ ├── auth_credentials.dart │ ├── notification_data.dart │ ├── active_session.dart │ └── category.dart ├── cubits │ ├── available_space_cubit.dart │ ├── profile_cubit.dart │ ├── change_email_verification_cubit.dart │ ├── forgot_password_cubit.dart │ ├── forgot_password_new_password_cubit.dart │ ├── login_cubit.dart │ ├── change_password_cubit.dart │ ├── email_verification_cubit.dart │ └── forgot_password_email_verification_cubit.dart ├── helpers │ ├── map_helper.dart │ ├── string_helper.dart │ ├── locale_helper.dart │ ├── response_messages.dart │ └── response_errors.dart ├── services │ ├── context_service.dart │ ├── locator_service.dart │ ├── notification_service.dart │ └── dialog_service.dart ├── blocs │ ├── sync_bloc │ │ ├── sync_event.dart │ │ ├── sync_bloc.g.dart │ │ └── sync_state.dart │ ├── transformers.dart │ ├── upcoming_bloc │ │ ├── upcoming_event.dart │ │ └── upcoming_state.dart │ ├── calendar_bloc │ │ ├── calendar_event.dart │ │ └── calendar_state.dart │ ├── category_bloc │ │ ├── category_event.dart │ │ ├── category_state.dart │ │ └── category_bloc.g.dart │ ├── category_screen_bloc │ │ ├── category_screen_event.dart │ │ └── category_screen_state.dart │ ├── notifications_cubit │ │ ├── notifications_cubit.g.dart │ │ └── notifications_state.dart │ ├── task_bloc │ │ ├── task_event.dart │ │ ├── task_state.dart │ │ └── task_bloc.g.dart │ ├── drifted_bloc │ │ ├── drifted_converters.dart │ │ ├── drifted_isolate.dart │ │ ├── drifted_database.dart │ │ └── drifted_storage.dart │ ├── auth_bloc │ │ ├── auth_state.dart │ │ ├── auth_event.dart │ │ └── auth_bloc.g.dart │ └── settings_cubit │ │ ├── settings_cubit.g.dart │ │ └── settings_state.dart ├── l10n │ └── l10n.dart ├── screens │ ├── splash_screen.dart │ └── main_screen.dart ├── messaging │ ├── data_notifications.dart │ ├── messaging_helper.dart │ ├── background_handler.dart │ └── types │ │ └── background_auth.dart ├── components │ ├── charts │ │ └── week_bar_chart_group_data.dart │ ├── lists │ │ ├── list_header.dart │ │ ├── list_item_animation.dart │ │ ├── dot_indicator_list.dart │ │ ├── snap_bounce_scroll_physics.dart │ │ ├── rounded_dismissible.dart │ │ ├── declarative_animated_list.dart │ │ └── animated_dynamic_task_list.dart │ ├── forms │ │ ├── form_input_header.dart │ │ ├── outlined_form_icon_button.dart │ │ ├── resend_code_timer.dart │ │ └── form_validator.dart │ ├── responsive │ │ ├── animated_widget_size.dart │ │ ├── widget_size.dart │ │ ├── fill_remaining_list.dart │ │ └── centered_page_view_widget.dart │ ├── popup_menu_icon_item.dart │ ├── tab_indicator.dart │ ├── header.dart │ ├── aligned_animated_switcher.dart │ ├── rounded_snack_bar.dart │ ├── center_text_icon_button.dart │ ├── animated_chip.dart │ ├── main │ │ ├── center_app_bar.dart │ │ └── app_bar.dart │ ├── calendar │ │ ├── calendar_group_hour.dart │ │ └── calendar_card.dart │ ├── dot_tab_bar.dart │ ├── shimmer │ │ └── shimmer_text.dart │ └── rounded_button.dart ├── repositories │ ├── interceptors │ │ └── unauthorized_interceptor.dart │ ├── user_repository.dart │ ├── task_repository.dart │ └── sync_repository.dart ├── main.dart ├── constants.dart ├── analysis_options.yaml ├── bottom_sheets │ ├── date_picker_bottom_sheet.dart │ ├── time_picker_bottom_sheet.dart │ └── modal_bottom_sheet.dart ├── router │ └── wrappers │ │ └── main_router_wrapper.dart └── firebase_options.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ └── Icon-512.png ├── splash │ ├── splash.js │ └── style.css └── manifest.json ├── assets ├── icons │ ├── email.png │ ├── google.png │ ├── facebook.png │ ├── profile.png │ ├── home_filled.png │ ├── home_outlined.png │ ├── calendar_filled.png │ ├── settings_filled.png │ ├── calendar_outlined.png │ ├── settings_outlined.png │ ├── notification_filled.png │ └── notification_outlined.png └── fonts │ ├── Poppins-Light.ttf │ ├── Poppins-Medium.ttf │ ├── Poppins-Regular.ttf │ └── Poppins-SemiBold.ttf ├── l10n.yaml ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-night │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-night-v21 │ │ │ │ │ ├── background.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values-v31 │ │ │ │ │ └── styles.xml │ │ │ │ ├── values-night-v31 │ │ │ │ │ └── styles.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── task_manager │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── .metadata ├── .vscode └── launch.json ├── .gitignore ├── README.md └── pubspec.yaml /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /lib/models/sync_status.dart: -------------------------------------------------------------------------------- 1 | enum SyncStatus { idle, pending } -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/models/sync_item_error.dart: -------------------------------------------------------------------------------- 1 | enum SyncErrorType { duplicatedId, blacklist } -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/web/favicon.png -------------------------------------------------------------------------------- /assets/icons/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/email.png -------------------------------------------------------------------------------- /assets/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/google.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /assets/icons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/facebook.png -------------------------------------------------------------------------------- /assets/icons/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/profile.png -------------------------------------------------------------------------------- /assets/icons/home_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/home_filled.png -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n/arb 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/fonts/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/fonts/Poppins-Light.ttf -------------------------------------------------------------------------------- /assets/icons/home_outlined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/home_outlined.png -------------------------------------------------------------------------------- /assets/fonts/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/fonts/Poppins-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/fonts/Poppins-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/fonts/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /assets/icons/calendar_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/calendar_filled.png -------------------------------------------------------------------------------- /assets/icons/settings_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/settings_filled.png -------------------------------------------------------------------------------- /assets/icons/calendar_outlined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/calendar_outlined.png -------------------------------------------------------------------------------- /assets/icons/settings_outlined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/settings_outlined.png -------------------------------------------------------------------------------- /assets/icons/notification_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/notification_filled.png -------------------------------------------------------------------------------- /assets/icons/notification_outlined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/assets/icons/notification_outlined.png -------------------------------------------------------------------------------- /lib/models/dynamic_object.dart: -------------------------------------------------------------------------------- 1 | class DynamicObject{ 2 | final dynamic object; 3 | 4 | DynamicObject({ 5 | required this.object, 6 | }); 7 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/android/app/src/main/res/drawable/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/android/app/src/main/res/drawable-v21/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/android/app/src/main/res/drawable-night/background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/android/app/src/main/res/drawable-night-v21/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/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/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/federicodesia/task_manager/HEAD/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/task_manager/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.task_manager 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /web/splash/splash.js: -------------------------------------------------------------------------------- 1 | function removeSplashFromWeb() { 2 | const elem = document.getElementById("splash"); 3 | if (elem) { 4 | elem.remove(); 5 | } 6 | document.body.style.background = "transparent"; 7 | } 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/cubits/available_space_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | 3 | class AvailableSpaceCubit extends Cubit { 4 | AvailableSpaceCubit() : super(0); 5 | 6 | void setHeight(double height) => emit(height); 7 | } -------------------------------------------------------------------------------- /lib/helpers/map_helper.dart: -------------------------------------------------------------------------------- 1 | extension MapExtension on Map { 2 | 3 | int increment(dynamic key) { 4 | return update( 5 | key, 6 | (value) => ++value, 7 | ifAbsent: () => 1 8 | ); 9 | } 10 | } -------------------------------------------------------------------------------- /lib/services/context_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContextService { 4 | 5 | BuildContext? _context; 6 | void init(BuildContext context) => _context = context; 7 | 8 | BuildContext? get context => _context; 9 | } -------------------------------------------------------------------------------- /lib/models/sliding_page.dart: -------------------------------------------------------------------------------- 1 | class SlidingPage{ 2 | final String header; 3 | final String description; 4 | final String svg; 5 | 6 | SlidingPage({ 7 | required this.header, 8 | required this.description, 9 | required this.svg 10 | }); 11 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/blocs/sync_bloc/sync_event.dart: -------------------------------------------------------------------------------- 1 | part of 'sync_bloc.dart'; 2 | 3 | abstract class SyncEvent {} 4 | 5 | class SyncLoaded extends SyncEvent {} 6 | 7 | class SyncRequested extends SyncEvent {} 8 | class BackgroundSyncRequested extends SyncEvent {} 9 | class HighPrioritySyncRequested extends SyncEvent {} -------------------------------------------------------------------------------- /lib/blocs/transformers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:rxdart/rxdart.dart'; 3 | 4 | EventTransformer debounceTransformer(Duration duration) { 5 | return (events, mapper) { 6 | return events.debounceTime(duration).switchMap(mapper); 7 | }; 8 | } -------------------------------------------------------------------------------- /lib/models/either.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'either.freezed.dart'; 4 | 5 | @freezed 6 | abstract class Either with _$Either { 7 | const factory Either.left(L left) = Left; 8 | const factory Either.right(R right) = Right; 9 | } -------------------------------------------------------------------------------- /lib/l10n/l10n.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | export 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | extension AppLocalizationsX on BuildContext { 7 | AppLocalizations get l10n => AppLocalizations.of(this)!; 8 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/blocs/upcoming_bloc/upcoming_event.dart: -------------------------------------------------------------------------------- 1 | part of 'upcoming_bloc.dart'; 2 | 3 | abstract class UpcomingEvent {} 4 | 5 | class UpcomingLoaded extends UpcomingEvent { 6 | UpcomingLoaded(); 7 | } 8 | 9 | class TasksUpdated extends UpcomingEvent { 10 | final List tasks; 11 | TasksUpdated(this.tasks); 12 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/helpers/string_helper.dart: -------------------------------------------------------------------------------- 1 | extension StringExtension on String { 2 | String get capitalize { 3 | return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; 4 | } 5 | 6 | String get toLowerSnakeCase => toLowerCase().replaceAll(" ", "_"); 7 | 8 | String fillLines(int lines) => this + List.generate(lines, (_) => "\n").join(); 9 | } -------------------------------------------------------------------------------- /.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: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/models/serializers/color_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | class ColorSerializer implements JsonConverter { 5 | const ColorSerializer(); 6 | 7 | @override 8 | Color fromJson(int value) => Color(value); 9 | 10 | @override 11 | int toJson(Color color) => color.value; 12 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/models/serializers/locale_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | class LocaleSerializer implements JsonConverter { 5 | const LocaleSerializer(); 6 | 7 | @override 8 | Locale? fromJson(String? languageCode) => languageCode != null ? Locale(languageCode) : null; 9 | 10 | @override 11 | String? toJson(Locale? locale) => locale?.languageCode; 12 | } -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /lib/helpers/locale_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_localized_locales/flutter_localized_locales.dart'; 3 | import 'package:task_manager/helpers/string_helper.dart'; 4 | 5 | export 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | 7 | extension LocaleExtension on Locale? { 8 | String get name{ 9 | final nativeLocaleNames = LocaleNamesLocalizationsDelegate.nativeLocaleNames; 10 | return (nativeLocaleNames[this?.languageCode] ?? "Unknown").capitalize; 11 | } 12 | } -------------------------------------------------------------------------------- /lib/services/locator_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:get_it/get_it.dart'; 2 | import 'package:task_manager/services/context_service.dart'; 3 | import 'package:task_manager/services/dialog_service.dart'; 4 | import 'package:task_manager/services/notification_service.dart'; 5 | 6 | GetIt locator = GetIt.instance; 7 | 8 | void setupLocator(){ 9 | locator.registerLazySingleton(() => (ContextService())); 10 | locator.registerLazySingleton(() => (DialogService())); 11 | locator.registerLazySingleton(() => (NotificationService())); 12 | } 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "LaunchImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "LaunchImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "LaunchImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/BrandingImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrandingImage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "BrandingImage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "BrandingImage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/models/task_filter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:task_manager/l10n/l10n.dart'; 3 | 4 | enum TaskFilter { all, completed, remaining } 5 | 6 | extension TaskFilterExtension on TaskFilter { 7 | String nameLocalization(BuildContext context) { 8 | if(this == TaskFilter.all) return context.l10n.enum_taskFilter_all; 9 | if(this == TaskFilter.completed) return context.l10n.enum_taskFilter_completed; 10 | if(this == TaskFilter.remaining) return context.l10n.enum_taskFilter_remaining; 11 | return "Unknown"; 12 | } 13 | } -------------------------------------------------------------------------------- /lib/screens/splash_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/theme/theme.dart'; 3 | 4 | class SplashScreen extends StatelessWidget{ 5 | const SplashScreen({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | final customTheme = Theme.of(context).customTheme; 10 | 11 | return Scaffold( 12 | backgroundColor: customTheme.backgroundColor, 13 | body: const SafeArea( 14 | child: Center( 15 | child: CircularProgressIndicator(), 16 | ), 17 | ), 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /lib/blocs/calendar_bloc/calendar_event.dart: -------------------------------------------------------------------------------- 1 | part of 'calendar_bloc.dart'; 2 | 3 | abstract class CalendarEvent {} 4 | 5 | class CalendarSelectedMonthChanged extends CalendarEvent { 6 | final DateTime selectedMonth; 7 | CalendarSelectedMonthChanged(this.selectedMonth); 8 | } 9 | 10 | class CalendarSelectedDayChanged extends CalendarEvent { 11 | final DateTime selectedDay; 12 | CalendarSelectedDayChanged(this.selectedDay); 13 | } 14 | 15 | class UpdateLoadingRequested extends CalendarEvent {} 16 | 17 | class TasksUpdated extends CalendarEvent { 18 | final List tasks; 19 | TasksUpdated(this.tasks); 20 | } -------------------------------------------------------------------------------- /lib/messaging/data_notifications.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | 3 | enum DataNotificationType { newData, logout, newUserData, newSession } 4 | 5 | extension RemoteMessageExtension on RemoteMessage { 6 | DataNotificationType? get dataNotificationType{ 7 | final type = data["type"]; 8 | 9 | if(type == "new-data") return DataNotificationType.newData; 10 | if(type == "logout") return DataNotificationType.logout; 11 | if(type == "new-user-data") return DataNotificationType.newUserData; 12 | if(type == "new-session") return DataNotificationType.newSession; 13 | return null; 14 | } 15 | } -------------------------------------------------------------------------------- /lib/components/charts/week_bar_chart_group_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:fl_chart/fl_chart.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | 4 | BarChartGroupData weekBarChartGroupData({ 5 | required int index, 6 | required double height, 7 | double backgroundHeight = 0.0 8 | }) { 9 | 10 | return BarChartGroupData( 11 | x: index, 12 | barRods: [ 13 | BarChartRodData( 14 | y: height, 15 | colors: [cPrimaryColor], 16 | backDrawRodData: BackgroundBarChartRodData( 17 | show: true, 18 | y: backgroundHeight, 19 | colors: [cChartBackgroundColor], 20 | ) 21 | ), 22 | ] 23 | ); 24 | } -------------------------------------------------------------------------------- /lib/components/lists/list_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/theme/theme.dart'; 3 | 4 | class ListHeader extends StatelessWidget{ 5 | 6 | final String text; 7 | 8 | const ListHeader( 9 | this.text, 10 | {Key? key} 11 | ) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final customTheme = Theme.of(context).customTheme; 16 | 17 | return Padding( 18 | padding: const EdgeInsets.symmetric(vertical: 16.0), 19 | child: Text( 20 | text, 21 | style: customTheme.boldTextStyle, 22 | maxLines: 1, 23 | ), 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task_manager", 3 | "short_name": "task_manager", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/blocs/category_bloc/category_event.dart: -------------------------------------------------------------------------------- 1 | part of 'category_bloc.dart'; 2 | 3 | abstract class CategoryEvent {} 4 | 5 | class CategoryLoaded extends CategoryEvent {} 6 | 7 | class CategoryAdded extends CategoryEvent { 8 | final Category category; 9 | CategoryAdded(this.category); 10 | } 11 | 12 | class CategoryUpdated extends CategoryEvent { 13 | final Category category; 14 | CategoryUpdated(this.category); 15 | } 16 | 17 | class CategoryDeleted extends CategoryEvent { 18 | final Category category; 19 | CategoryDeleted(this.category); 20 | } 21 | 22 | class CategoryStateUpdated extends CategoryEvent { 23 | final CategoryState state; 24 | CategoryStateUpdated(this.state); 25 | } -------------------------------------------------------------------------------- /lib/blocs/upcoming_bloc/upcoming_state.dart: -------------------------------------------------------------------------------- 1 | part of 'upcoming_bloc.dart'; 2 | 3 | abstract class UpcomingState {} 4 | 5 | class UpcomingLoadInProgress extends UpcomingState {} 6 | 7 | class UpcomingLoadSuccess extends UpcomingState { 8 | final Map weekTasks; 9 | final Map weekCompletedTasks; 10 | final int weekCompletedTasksCount; 11 | final int weekRemainingTasksCount; 12 | final List items; 13 | 14 | UpcomingLoadSuccess({ 15 | required this.weekTasks, 16 | required this.weekCompletedTasks, 17 | required this.weekCompletedTasksCount, 18 | required this.weekRemainingTasksCount, 19 | required this.items 20 | }); 21 | } -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /lib/components/forms/form_input_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/theme/theme.dart'; 3 | 4 | class FormInputHeader extends StatelessWidget{ 5 | 6 | final String text; 7 | const FormInputHeader( 8 | this.text, 9 | {Key? key} 10 | ) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final customTheme = Theme.of(context).customTheme; 15 | 16 | return Padding( 17 | padding: const EdgeInsets.only( 18 | top: 24.0, 19 | bottom: 8.0 20 | ), 21 | child: Text( 22 | text, 23 | style: customTheme.boldTextStyle, 24 | maxLines: 1, 25 | ), 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.10' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /lib/blocs/category_screen_bloc/category_screen_event.dart: -------------------------------------------------------------------------------- 1 | part of 'category_screen_bloc.dart'; 2 | 3 | abstract class CategoryScreenEvent {} 4 | 5 | class CategoryScreenLoaded extends CategoryScreenEvent {} 6 | 7 | class SearchTextChanged extends CategoryScreenEvent { 8 | final String searchText; 9 | SearchTextChanged(this.searchText); 10 | } 11 | 12 | class FilterUpdated extends CategoryScreenEvent { 13 | final TaskFilter taskFilter; 14 | FilterUpdated(this.taskFilter); 15 | } 16 | 17 | class UpdateItemsRequested extends CategoryScreenEvent{ 18 | final String? searchText; 19 | final TaskFilter? taskFilter; 20 | final List? tasks; 21 | 22 | UpdateItemsRequested({ 23 | this.searchText, 24 | this.taskFilter, 25 | this.tasks 26 | }); 27 | } -------------------------------------------------------------------------------- /lib/blocs/sync_bloc/sync_bloc.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'sync_bloc.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SyncState _$SyncStateFromJson(Map json) => SyncState( 10 | userId: json['userId'] as String?, 11 | lastSync: const NullableDateTimeSerializer() 12 | .fromJson(json['lastSync'] as String?), 13 | ); 14 | 15 | Map _$SyncStateToJson(SyncState instance) => { 16 | 'userId': instance.userId, 17 | 'lastSync': const NullableDateTimeSerializer().toJson(instance.lastSync), 18 | }; 19 | -------------------------------------------------------------------------------- /lib/components/responsive/animated_widget_size.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | 4 | class AnimatedWidgetSize extends StatelessWidget { 5 | final Duration duration; 6 | final Curve curve; 7 | final Alignment alignment; 8 | final Widget child; 9 | 10 | const AnimatedWidgetSize({ 11 | Key? key, 12 | this.duration = cTransitionDuration, 13 | this.curve = Curves.linear, 14 | this.alignment = Alignment.topLeft, 15 | required this.child, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return AnimatedSize( 21 | duration: duration, 22 | curve: curve, 23 | alignment: alignment, 24 | child: child, 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/blocs/sync_bloc/sync_state.dart: -------------------------------------------------------------------------------- 1 | part of 'sync_bloc.dart'; 2 | 3 | @JsonSerializable() 4 | class SyncState { 5 | 6 | final String? userId; 7 | @NullableDateTimeSerializer() 8 | final DateTime? lastSync; 9 | 10 | SyncState({ 11 | required this.userId, 12 | required this.lastSync 13 | }); 14 | 15 | static SyncState get initial => SyncState( 16 | userId: null, 17 | lastSync: null 18 | ); 19 | 20 | SyncState copyWith({ 21 | String? userId, 22 | DateTime? lastSync 23 | }){ 24 | return SyncState( 25 | userId: userId ?? this.userId, 26 | lastSync: lastSync ?? this.lastSync 27 | ); 28 | } 29 | 30 | factory SyncState.fromJson(Map json) => _$SyncStateFromJson(json); 31 | Map toJson() => _$SyncStateToJson(this); 32 | } -------------------------------------------------------------------------------- /web/splash/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | height:100%; 4 | background: #F0F2F5; 5 | 6 | background-size: 100% 100%; 7 | } 8 | 9 | .center { 10 | margin: 0; 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | -ms-transform: translate(-50%, -50%); 15 | transform: translate(-50%, -50%); 16 | } 17 | 18 | .contain { 19 | display:block; 20 | width:100%; height:100%; 21 | object-fit: contain; 22 | } 23 | 24 | .stretch { 25 | display:block; 26 | width:100%; height:100%; 27 | } 28 | 29 | .cover { 30 | display:block; 31 | width:100%; height:100%; 32 | object-fit: cover; 33 | } 34 | 35 | @media (prefers-color-scheme: dark) { 36 | body { 37 | margin:0; 38 | height:100%; 39 | background: #1F1D2C; 40 | 41 | background-size: 100% 100%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/blocs/notifications_cubit/notifications_cubit.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'notifications_cubit.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NotificationsState _$NotificationsStateFromJson(Map json) => 10 | NotificationsState( 11 | notifications: (json['notifications'] as List) 12 | .map((e) => NotificationData.fromJson(e as Map)) 13 | .toList(), 14 | ); 15 | 16 | Map _$NotificationsStateToJson(NotificationsState instance) => 17 | { 18 | 'notifications': instance.notifications, 19 | }; 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "task_manager", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "task_manager (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "task_manager (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /lib/repositories/interceptors/unauthorized_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 3 | import 'package:task_manager/models/auth_credentials.dart'; 4 | 5 | class UnauthorizedInterceptor extends Interceptor { 6 | 7 | final AuthBloc authBloc; 8 | UnauthorizedInterceptor({required this.authBloc}); 9 | 10 | @override 11 | void onError(DioError err, ErrorInterceptorHandler handler) async { 12 | try{ 13 | final statusCode = err.response?.statusCode; 14 | if(statusCode == 401 || statusCode == 403){ 15 | 16 | if(authBloc.state.status == AuthStatus.authenticated){ 17 | authBloc.add(AuthCredentialsChanged(AuthCredentials.empty)); 18 | } 19 | } 20 | } 21 | catch(_) {} 22 | return super.onError(err, handler); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/components/popup_menu_icon_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/theme/theme.dart'; 3 | 4 | class PopupMenuIconItem extends StatelessWidget{ 5 | 6 | final IconData icon; 7 | final String text; 8 | 9 | const PopupMenuIconItem({ 10 | Key? key, 11 | required this.icon, 12 | required this.text 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | final customTheme = Theme.of(context).customTheme; 18 | 19 | return Row( 20 | children: [ 21 | Icon( 22 | icon, 23 | color: customTheme.lightColor, 24 | ), 25 | const SizedBox(width: 12.0), 26 | Text( 27 | text, 28 | style: customTheme.lightTextStyle, 29 | maxLines: 1, 30 | ) 31 | ], 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:task_manager/models/serializers/datetime_serializer.dart'; 3 | 4 | part 'user.g.dart'; 5 | 6 | @JsonSerializable() 7 | class User{ 8 | final String id; 9 | final String email; 10 | final String name; 11 | final String? imageUrl; 12 | @DateTimeSerializer() 13 | final DateTime createdAt; 14 | @DateTimeSerializer() 15 | final DateTime updatedAt; 16 | final bool verified; 17 | 18 | const User({ 19 | required this.id, 20 | required this.email, 21 | required this.name, 22 | this.imageUrl, 23 | required this.createdAt, 24 | required this.updatedAt, 25 | required this.verified, 26 | }); 27 | 28 | factory User.fromJson(Map json) => _$UserFromJson(json); 29 | Map toJson() => _$UserToJson(this); 30 | } -------------------------------------------------------------------------------- /lib/blocs/task_bloc/task_event.dart: -------------------------------------------------------------------------------- 1 | part of 'task_bloc.dart'; 2 | 3 | abstract class TaskEvent {} 4 | 5 | class TaskLoaded extends TaskEvent {} 6 | 7 | class TaskAdded extends TaskEvent { 8 | final Task task; 9 | TaskAdded(this.task); 10 | } 11 | 12 | class TaskUpdated extends TaskEvent { 13 | final Task task; 14 | TaskUpdated(this.task); 15 | } 16 | 17 | class TaskDeleted extends TaskEvent { 18 | final Task task; 19 | TaskDeleted(this.task); 20 | } 21 | 22 | class TaskUndoDeleted extends TaskEvent { 23 | final Task task; 24 | TaskUndoDeleted(this.task); 25 | } 26 | 27 | class TasksUpdated extends TaskEvent { 28 | final List tasks; 29 | TasksUpdated(this.tasks); 30 | } 31 | 32 | class TaskStateUpdated extends TaskEvent { 33 | final TaskState state; 34 | TaskStateUpdated(this.state); 35 | } 36 | 37 | class ScheduleTaskNotificationsRequested extends TaskEvent {} -------------------------------------------------------------------------------- /lib/models/auth_credentials.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_credentials.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AuthCredentials _$AuthCredentialsFromJson(Map json) => 10 | AuthCredentials( 11 | refreshToken: json['refreshToken'] as String? ?? "", 12 | accessToken: json['accessToken'] as String? ?? "", 13 | passwordToken: json['passwordToken'] as String? ?? "", 14 | ); 15 | 16 | Map _$AuthCredentialsToJson(AuthCredentials instance) => 17 | { 18 | 'refreshToken': instance.refreshToken, 19 | 'accessToken': instance.accessToken, 20 | 'passwordToken': instance.passwordToken, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/blocs/category_screen_bloc/category_screen_state.dart: -------------------------------------------------------------------------------- 1 | part of 'category_screen_bloc.dart'; 2 | 3 | class CategoryScreenState { 4 | final String searchText; 5 | final TaskFilter activeFilter; 6 | final List items; 7 | 8 | CategoryScreenState({ 9 | required this.searchText, 10 | required this.activeFilter, 11 | required this.items 12 | }); 13 | 14 | static CategoryScreenState get initial => CategoryScreenState( 15 | searchText: "", 16 | activeFilter: TaskFilter.all, 17 | items: [] 18 | ); 19 | 20 | CategoryScreenState copyWith({ 21 | String? searchText, 22 | TaskFilter? activeFilter, 23 | List? items 24 | }){ 25 | return CategoryScreenState( 26 | searchText: searchText ?? this.searchText, 27 | activeFilter: activeFilter ?? this.activeFilter, 28 | items: items ?? this.items, 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/models/serializers/datetime_serializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | class DateTimeSerializer implements JsonConverter { 5 | const DateTimeSerializer(); 6 | 7 | @override 8 | DateTime fromJson(String dateUtc) => DateFormat("yyyy-MM-ddTHH:mm:ss.SSS").parse(dateUtc, true).toLocal(); 9 | 10 | @override 11 | String toJson(DateTime localDate) => DateFormat("yyyy-MM-ddTHH:mm:ss.SSS").format(localDate.toUtc()) + "Z"; 12 | } 13 | 14 | class NullableDateTimeSerializer implements JsonConverter { 15 | const NullableDateTimeSerializer(); 16 | 17 | @override 18 | DateTime? fromJson(String? dateUtc) => dateUtc != null ? const DateTimeSerializer().fromJson(dateUtc) : null; 19 | 20 | @override 21 | String? toJson(DateTime? localDate) => localDate != null ? const DateTimeSerializer().toJson(localDate) : null; 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /lib/messaging/messaging_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | abstract class MessagingHelper{ 7 | 8 | static void autoCloseBlocs({ 9 | required List blocs, 10 | Function()? onClosed 11 | }) async { 12 | await Future.any( 13 | blocs.map((bloc) => bloc.stream.first) 14 | ) 15 | .timeout(const Duration(seconds: 10), onTimeout: () { 16 | throw TimeoutException("AutoCloseBlocs timeout"); 17 | }) 18 | .then((_){ 19 | autoCloseBlocs( 20 | blocs: blocs, 21 | onClosed: onClosed 22 | ); 23 | }) 24 | .onError((error, _) { 25 | if(error is TimeoutException) { 26 | debugPrint("Auto closing all blocs..."); 27 | 28 | for (BlocBase bloc in blocs) { 29 | bloc.close(); 30 | } 31 | if(onClosed != null) onClosed(); 32 | } 33 | }); 34 | } 35 | } -------------------------------------------------------------------------------- /lib/blocs/drifted_bloc/drifted_converters.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:drift/drift.dart'; 4 | 5 | class DateTimeConverter extends TypeConverter { 6 | const DateTimeConverter(); 7 | 8 | @override 9 | DateTime? mapToDart(int? fromDb) { 10 | if (fromDb == null) return null; 11 | return DateTime.fromMicrosecondsSinceEpoch(fromDb); 12 | } 13 | 14 | @override 15 | int? mapToSql(DateTime? value) { 16 | if (value == null) return null; 17 | return value.microsecondsSinceEpoch; 18 | } 19 | } 20 | 21 | class StateConverter extends TypeConverter?, String> { 22 | const StateConverter(); 23 | 24 | @override 25 | Map? mapToDart(String? fromDb) { 26 | if (fromDb == null) return null; 27 | return jsonDecode(fromDb); 28 | } 29 | 30 | @override 31 | String? mapToSql(Map? value) { 32 | if (value == null) return null; 33 | return jsonEncode(value); 34 | } 35 | } -------------------------------------------------------------------------------- /lib/components/lists/list_item_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListItemAnimation extends StatelessWidget{ 4 | 5 | final Animation animation; 6 | final Axis axis; 7 | final Widget child; 8 | 9 | const ListItemAnimation({ 10 | Key? key, 11 | required this.animation, 12 | this.axis = Axis.vertical, 13 | required this.child, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return FadeTransition( 19 | opacity: Tween(begin: 0.0, end: 1.0).animate( 20 | CurvedAnimation( 21 | parent: animation, 22 | curve: const Interval(0.75, 1.0, curve: Curves.easeInOut), 23 | ), 24 | ), 25 | child: SizeTransition( 26 | axis: axis, 27 | sizeFactor: CurvedAnimation( 28 | parent: animation, 29 | curve: Curves.fastOutSlowIn 30 | ), 31 | child: child, 32 | ) 33 | ); 34 | } 35 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:firebase_messaging/firebase_messaging.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:task_manager/app.dart'; 5 | import 'package:task_manager/blocs/drifted_bloc/drifted_bloc.dart'; 6 | import 'package:task_manager/blocs/drifted_bloc/drifted_storage.dart'; 7 | import 'package:task_manager/firebase_options.dart'; 8 | import 'package:task_manager/messaging/background_handler.dart'; 9 | import 'package:task_manager/services/locator_service.dart'; 10 | 11 | void main() async{ 12 | Paint.enableDithering = true; 13 | setupLocator(); 14 | 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 17 | FirebaseMessaging.onBackgroundMessage(BackgroundHandler.onBackgroundMessage); 18 | 19 | final storage = await DriftedStorage.build(); 20 | DriftedBlocOverrides.runZoned( 21 | () => runApp(const MyApp()), 22 | storage: storage 23 | ); 24 | } -------------------------------------------------------------------------------- /lib/blocs/auth_bloc/auth_state.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | enum AuthStatus { loading, unauthenticated, waitingVerification, authenticated } 4 | 5 | @JsonSerializable() 6 | class AuthState { 7 | 8 | final AuthStatus status; 9 | final User? user; 10 | final List activeSessions; 11 | 12 | AuthState({ 13 | required this.status, 14 | required this.user, 15 | required this.activeSessions, 16 | }); 17 | 18 | static AuthState get initial => AuthState( 19 | status: AuthStatus.loading, 20 | user: null, 21 | activeSessions: [] 22 | ); 23 | 24 | AuthState copyWith({ 25 | AuthStatus? status, 26 | User? user, 27 | List? activeSessions 28 | }){ 29 | return AuthState( 30 | status: status ?? this.status, 31 | user: user ?? this.user, 32 | activeSessions: activeSessions ?? this.activeSessions 33 | ); 34 | } 35 | 36 | factory AuthState.fromJson(Map json) => _$AuthStateFromJson(json); 37 | Map toJson() => _$AuthStateToJson(this); 38 | } -------------------------------------------------------------------------------- /lib/components/tab_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | 4 | class TabIndicatorDecoration extends Decoration{ 5 | @override 6 | BoxPainter createBoxPainter([VoidCallback? onChanged]) { 7 | return TabIndicatorPainter(); 8 | } 9 | } 10 | 11 | class TabIndicatorPainter extends BoxPainter{ 12 | @override 13 | void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { 14 | 15 | final tabWidth = configuration.size!.width; 16 | final tabHeight = configuration.size!.height; 17 | 18 | Rect rect = Rect.fromCenter( 19 | center: Offset( 20 | offset.dx + tabWidth / 2, 21 | offset.dy + tabHeight / 2 22 | ), 23 | width: tabWidth, 24 | height: tabHeight 25 | ); 26 | 27 | final Paint paint = Paint(); 28 | paint.color = cPrimaryColor; 29 | paint.style = PaintingStyle.fill; 30 | 31 | canvas.drawRRect( 32 | RRect.fromRectAndRadius(rect, const Radius.circular(cBorderRadius)), 33 | paint 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | -------------------------------------------------------------------------------- /lib/components/header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class Header extends StatelessWidget{ 6 | 7 | final String text; 8 | final String? rightText; 9 | 10 | const Header({ 11 | Key? key, 12 | required this.text, 13 | this.rightText 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final customTheme = Theme.of(context).customTheme; 19 | 20 | return Padding( 21 | padding: const EdgeInsets.symmetric(horizontal: cPadding), 22 | child: Row( 23 | children: [ 24 | Expanded( 25 | child: Text( 26 | text, 27 | style: customTheme.boldTextStyle, 28 | maxLines: 1 29 | ), 30 | ), 31 | 32 | Text( 33 | rightText ?? "", 34 | style: customTheme.lightTextStyle, 35 | maxLines: 1 36 | ) 37 | ], 38 | ), 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values-night-v31/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | -------------------------------------------------------------------------------- /lib/messaging/background_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:firebase_messaging/firebase_messaging.dart'; 5 | import 'package:task_manager/messaging/data_notifications.dart'; 6 | import 'package:task_manager/messaging/types/background_auth.dart'; 7 | import 'package:task_manager/messaging/types/background_sync.dart'; 8 | 9 | abstract class BackgroundHandler{ 10 | 11 | static Future onBackgroundMessage(RemoteMessage message) async{ 12 | try{ 13 | await Firebase.initializeApp(); 14 | final type = message.dataNotificationType; 15 | if(type == null) return; 16 | 17 | if(type == DataNotificationType.newData){ 18 | BackgroundSync.backgroundSyncHandler(message); 19 | } 20 | else if(type == DataNotificationType.logout 21 | || type == DataNotificationType.newUserData 22 | || type == DataNotificationType.newUserData 23 | ){ 24 | BackgroundAuth.backgroundAuthHandler(message, type); 25 | } 26 | } 27 | catch(_){} 28 | } 29 | } -------------------------------------------------------------------------------- /lib/components/responsive/widget_size.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | 4 | class WidgetSize extends StatefulWidget { 5 | final Widget child; 6 | final Function onChange; 7 | 8 | const WidgetSize({ 9 | Key? key, 10 | required this.child, 11 | required this.onChange, 12 | }) : super(key: key); 13 | 14 | @override 15 | _WidgetSizeState createState() => _WidgetSizeState(); 16 | } 17 | 18 | class _WidgetSizeState extends State { 19 | @override 20 | Widget build(BuildContext context) { 21 | SchedulerBinding.instance!.addPostFrameCallback(postFrameCallback); 22 | return Container( 23 | key: widgetKey, 24 | child: widget.child, 25 | ); 26 | } 27 | 28 | GlobalKey widgetKey = GlobalKey(); 29 | Size? oldSize; 30 | 31 | void postFrameCallback(_) { 32 | var context = widgetKey.currentContext; 33 | if (context == null) return; 34 | 35 | Size? newSize = context.size; 36 | if (oldSize == newSize) return; 37 | 38 | oldSize = newSize; 39 | widget.onChange(newSize); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/models/geo_location.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | import 'package:task_manager/models/serializers/datetime_serializer.dart'; 4 | 5 | part 'geo_location.g.dart'; 6 | 7 | @JsonSerializable() 8 | class GeoLocation extends Equatable{ 9 | 10 | final int id; 11 | final String? country; 12 | final String? countryCode; 13 | final String? region; 14 | final String? city; 15 | final double? lat; 16 | final double? lon; 17 | @DateTimeSerializer() 18 | final DateTime createdAt; 19 | 20 | const GeoLocation({ 21 | required this.id, 22 | this.country, 23 | this.countryCode, 24 | this.region, 25 | this.city, 26 | this.lat, 27 | this.lon, 28 | required this.createdAt 29 | }); 30 | 31 | factory GeoLocation.fromJson(Map json) => _$GeoLocationFromJson(json); 32 | Map toJson() => _$GeoLocationToJson(this); 33 | 34 | @override 35 | List get props => [id, country, countryCode, region, city, lat, lon, createdAt]; 36 | 37 | @override 38 | bool get stringify => true; 39 | } -------------------------------------------------------------------------------- /lib/blocs/auth_bloc/auth_event.dart: -------------------------------------------------------------------------------- 1 | part of 'auth_bloc.dart'; 2 | 3 | abstract class AuthEvent {} 4 | 5 | class DataNotificationReceived extends AuthEvent { 6 | final DataNotificationType? type; 7 | DataNotificationReceived(this.type); 8 | } 9 | 10 | class AuthLoaded extends AuthEvent {} 11 | 12 | class AuthCredentialsChanged extends AuthEvent { 13 | final AuthCredentials credentials; 14 | AuthCredentialsChanged(this.credentials); 15 | } 16 | 17 | class AuthUserChanged extends AuthEvent { 18 | final User user; 19 | AuthUserChanged(this.user); 20 | } 21 | class UpdateUserRequested extends AuthEvent {} 22 | 23 | class AuthLogoutRequested extends AuthEvent {} 24 | class AuthLogoutAllRequested extends AuthEvent {} 25 | class AuthLogoutSessionRequested extends AuthEvent { 26 | final int sessionId; 27 | AuthLogoutSessionRequested(this.sessionId); 28 | } 29 | 30 | class NotifyNewSessionRequested extends AuthEvent {} 31 | class UpdateActiveSessionsRequested extends AuthEvent {} 32 | 33 | class UpdateFirebaseMessagingTokenRequested extends AuthEvent { 34 | final String? token; 35 | UpdateFirebaseMessagingTokenRequested(this.token); 36 | } -------------------------------------------------------------------------------- /lib/models/bottom_navigation_bar_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/screens/calendar_screen.dart'; 3 | import 'package:task_manager/screens/home/home_screen.dart'; 4 | 5 | class MyBottomNavigationBarItem{ 6 | final String icon; 7 | final String selectedIcon; 8 | final Widget child; 9 | 10 | MyBottomNavigationBarItem({ 11 | required this.icon, 12 | required this.selectedIcon, 13 | required this.child 14 | }); 15 | } 16 | 17 | List bottomNavigationBarItems = [ 18 | MyBottomNavigationBarItem( 19 | icon: "home_outlined", 20 | selectedIcon: "home_filled", 21 | child: const HomeScreen() 22 | ), 23 | 24 | MyBottomNavigationBarItem( 25 | icon: "calendar_outlined", 26 | selectedIcon: "calendar_filled", 27 | child: const CalendarScreen() 28 | ), 29 | 30 | MyBottomNavigationBarItem( 31 | icon: "notification_outlined", 32 | selectedIcon: "notification_filled", 33 | child: Container() 34 | ), 35 | 36 | MyBottomNavigationBarItem( 37 | icon: "settings_outlined", 38 | selectedIcon: "settings_filled", 39 | child: Container() 40 | ) 41 | ]; -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "darkbackground.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "appearances" : [ 25 | { 26 | "appearance" : "luminosity", 27 | "value" : "dark" 28 | } 29 | ], 30 | "idiom" : "universal", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "universal", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "appearances" : [ 39 | { 40 | "appearance" : "luminosity", 41 | "value" : "dark" 42 | } 43 | ], 44 | "idiom" : "universal", 45 | "scale" : "3x" 46 | } 47 | ], 48 | "info" : { 49 | "author" : "xcode", 50 | "version" : 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "209326263434", 4 | "project_id": "task-manager-1d40c", 5 | "storage_bucket": "task-manager-1d40c.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:209326263434:android:2fe70e2f07127221bc7173", 11 | "android_client_info": { 12 | "package_name": "com.example.task_manager" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "209326263434-nmjv5hasfn7u13fonaj7547irul26cv8.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyCPH9lXTMBXm1iDMXqZByWq2Vt6FsKDyIU" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "209326263434-nmjv5hasfn7u13fonaj7547irul26cv8.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /lib/components/aligned_animated_switcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | 4 | class AlignedAnimatedSwitcher extends StatefulWidget{ 5 | 6 | final Widget child; 7 | final Alignment alignment; 8 | final Duration duration; 9 | 10 | const AlignedAnimatedSwitcher({ 11 | Key? key, 12 | required this.child, 13 | this.alignment = Alignment.topLeft, 14 | this.duration = cTransitionDuration 15 | }) : super(key: key); 16 | 17 | @override 18 | _AlignedAnimatedSwitcherState createState() => _AlignedAnimatedSwitcherState(); 19 | } 20 | 21 | class _AlignedAnimatedSwitcherState extends State{ 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return AnimatedSwitcher( 26 | duration: widget.duration, 27 | layoutBuilder: (Widget? currentChild, List previousChildren) { 28 | return Stack( 29 | children: [ 30 | ...previousChildren, 31 | if(currentChild != null) currentChild, 32 | ], 33 | alignment: widget.alignment, 34 | ); 35 | }, 36 | child: widget.child, 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /lib/models/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) => User( 10 | id: json['id'] as String, 11 | email: json['email'] as String, 12 | name: json['name'] as String, 13 | imageUrl: json['imageUrl'] as String?, 14 | createdAt: 15 | const DateTimeSerializer().fromJson(json['createdAt'] as String), 16 | updatedAt: 17 | const DateTimeSerializer().fromJson(json['updatedAt'] as String), 18 | verified: json['verified'] as bool, 19 | ); 20 | 21 | Map _$UserToJson(User instance) => { 22 | 'id': instance.id, 23 | 'email': instance.email, 24 | 'name': instance.name, 25 | 'imageUrl': instance.imageUrl, 26 | 'createdAt': const DateTimeSerializer().toJson(instance.createdAt), 27 | 'updatedAt': const DateTimeSerializer().toJson(instance.updatedAt), 28 | 'verified': instance.verified, 29 | }; 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | -------------------------------------------------------------------------------- /lib/models/notification_data.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'notification_data.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NotificationData _$NotificationDataFromJson(Map json) => 10 | NotificationData( 11 | id: json['id'] as int, 12 | title: json['title'] as String, 13 | body: json['body'] as String, 14 | createdAt: DateTime.parse(json['createdAt'] as String), 15 | scheduledAt: json['scheduledAt'] == null 16 | ? null 17 | : DateTime.parse(json['scheduledAt'] as String), 18 | type: NotificationType.fromJson(json['type'] as Map), 19 | ); 20 | 21 | Map _$NotificationDataToJson(NotificationData instance) => 22 | { 23 | 'id': instance.id, 24 | 'title': instance.title, 25 | 'body': instance.body, 26 | 'createdAt': instance.createdAt.toIso8601String(), 27 | 'scheduledAt': instance.scheduledAt?.toIso8601String(), 28 | 'type': instance.type, 29 | }; 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 20 | -------------------------------------------------------------------------------- /lib/components/rounded_snack_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class RoundedSnackBar{ 6 | 7 | final BuildContext context; 8 | final String text; 9 | final SnackBarAction action; 10 | 11 | RoundedSnackBar({ 12 | required this.context, 13 | required this.text, 14 | required this.action 15 | }); 16 | 17 | void show(){ 18 | final customTheme = Theme.of(context).customTheme; 19 | 20 | ScaffoldMessenger.of(context).showSnackBar( 21 | SnackBar( 22 | behavior: SnackBarBehavior.floating, 23 | margin: const EdgeInsets.all(cPadding), 24 | padding: const EdgeInsets.fromLTRB(cPadding, 4.0, 6.0, 4.0), 25 | backgroundColor: customTheme.contentBackgroundColor, 26 | elevation: 2.0, 27 | shape: const RoundedRectangleBorder( 28 | borderRadius: BorderRadius.all(Radius.circular(16.0)) 29 | ), 30 | content: Text( 31 | text, 32 | style: customTheme.smallTextStyle.copyWith(height: 1.0), 33 | maxLines: 1, 34 | ), 35 | action: action, 36 | ) 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /lib/blocs/auth_bloc/auth_bloc.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_bloc.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | AuthState _$AuthStateFromJson(Map json) => AuthState( 10 | status: $enumDecode(_$AuthStatusEnumMap, json['status']), 11 | user: json['user'] == null 12 | ? null 13 | : User.fromJson(json['user'] as Map), 14 | activeSessions: (json['activeSessions'] as List) 15 | .map((e) => ActiveSession.fromJson(e as Map)) 16 | .toList(), 17 | ); 18 | 19 | Map _$AuthStateToJson(AuthState instance) => { 20 | 'status': _$AuthStatusEnumMap[instance.status], 21 | 'user': instance.user, 22 | 'activeSessions': instance.activeSessions, 23 | }; 24 | 25 | const _$AuthStatusEnumMap = { 26 | AuthStatus.loading: 'loading', 27 | AuthStatus.unauthenticated: 'unauthenticated', 28 | AuthStatus.waitingVerification: 'waitingVerification', 29 | AuthStatus.authenticated: 'authenticated', 30 | }; 31 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const double cPadding = 24.0; 4 | const double cBorderRadius = 18.0; 5 | const double cLineSize = 6.0; 6 | const double cDotSize = 7.0; 7 | const double cSelectedDotSize = 14.0; 8 | 9 | // Colors 10 | const Color cPrimaryColor = Color(0xFF5C5D9D); 11 | const Color cTextButtonColor = Color(0xFF9E6AF8); 12 | 13 | const Color cRedColor = Color(0xFFD32F2F); 14 | const Color cGoldenColor = Color(0xFFFFBF47); 15 | const Color cBarrierColor = Colors.black26; 16 | 17 | Color cChartBackgroundColor = cPrimaryColor.withOpacity(0.64); 18 | 19 | // Buttons 20 | const double cButtonSize = 48.0; 21 | const double cButtonPadding = 10.0; 22 | 23 | const double cSplashRadius = 32.0; 24 | const double cSmallSplashRadius = 24.0; 25 | 26 | // List Items 27 | const double cListItemPadding = 18.0; 28 | const double cListItemSpace = 12.0; 29 | 30 | // Animations 31 | const Duration cFastAnimationDuration = Duration(milliseconds: 150); 32 | const Duration cTransitionDuration = Duration(milliseconds: 300); 33 | const Duration cAnimationDuration = Duration(milliseconds: 500); 34 | const Duration cAnimatedListDuration = Duration(milliseconds: 600); 35 | 36 | // BottomSheet 37 | const double cBottomSheetBorderRadius = 28.0; -------------------------------------------------------------------------------- /lib/models/geo_location.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'geo_location.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GeoLocation _$GeoLocationFromJson(Map json) => GeoLocation( 10 | id: json['id'] as int, 11 | country: json['country'] as String?, 12 | countryCode: json['countryCode'] as String?, 13 | region: json['region'] as String?, 14 | city: json['city'] as String?, 15 | lat: (json['lat'] as num?)?.toDouble(), 16 | lon: (json['lon'] as num?)?.toDouble(), 17 | createdAt: 18 | const DateTimeSerializer().fromJson(json['createdAt'] as String), 19 | ); 20 | 21 | Map _$GeoLocationToJson(GeoLocation instance) => 22 | { 23 | 'id': instance.id, 24 | 'country': instance.country, 25 | 'countryCode': instance.countryCode, 26 | 'region': instance.region, 27 | 'city': instance.city, 28 | 'lat': instance.lat, 29 | 'lon': instance.lon, 30 | 'createdAt': const DateTimeSerializer().toJson(instance.createdAt), 31 | }; 32 | -------------------------------------------------------------------------------- /lib/repositories/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:task_manager/helpers/response_errors.dart'; 4 | import 'package:task_manager/models/user.dart'; 5 | import 'package:task_manager/repositories/base_repository.dart'; 6 | 7 | class UserRepository { 8 | 9 | final BaseRepository base; 10 | UserRepository({required this.base}); 11 | 12 | Future getUser() async { 13 | 14 | try{ 15 | final dio = await base.dioAccessToken; 16 | final response = await dio.get("/user/"); 17 | return User.fromJson(response.data); 18 | } 19 | catch (error){ 20 | await ResponseError.validate(error, null); 21 | return null; 22 | } 23 | } 24 | 25 | Future updateUser({ 26 | String? name, 27 | String? imageUrl 28 | }) async { 29 | 30 | try{ 31 | final dio = await base.dioAccessToken; 32 | final response = await dio.patch( 33 | "/user/", 34 | data: { 35 | if(name != null) "name": name, 36 | if(imageUrl != null) "imageUrl": imageUrl 37 | } 38 | ); 39 | return User.fromJson(response.data); 40 | } 41 | catch (error){ 42 | await ResponseError.validate(error, null); 43 | return null; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /lib/models/category.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'category.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Category _$CategoryFromJson(Map json) => Category( 10 | id: json['id'] as String?, 11 | name: json['name'] as String, 12 | color: const ColorSerializer().fromJson(json['color'] as int), 13 | createdAt: 14 | const DateTimeSerializer().fromJson(json['createdAt'] as String), 15 | updatedAt: 16 | const DateTimeSerializer().fromJson(json['updatedAt'] as String), 17 | deletedAt: const NullableDateTimeSerializer() 18 | .fromJson(json['deletedAt'] as String?), 19 | ); 20 | 21 | Map _$CategoryToJson(Category instance) => { 22 | 'id': instance.id, 23 | 'name': instance.name, 24 | 'color': const ColorSerializer().toJson(instance.color), 25 | 'createdAt': const DateTimeSerializer().toJson(instance.createdAt), 26 | 'updatedAt': const DateTimeSerializer().toJson(instance.updatedAt), 27 | 'deletedAt': 28 | const NullableDateTimeSerializer().toJson(instance.deletedAt), 29 | }; 30 | -------------------------------------------------------------------------------- /lib/components/lists/dot_indicator_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class DotIndicatorList extends StatelessWidget{ 6 | 7 | final int count; 8 | final int selectedIndex; 9 | 10 | const DotIndicatorList({ 11 | Key? key, 12 | required this.count, 13 | required this.selectedIndex 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final customTheme = Theme.of(context).customTheme; 19 | 20 | return Row( 21 | mainAxisAlignment: MainAxisAlignment.center, 22 | mainAxisSize: MainAxisSize.min, 23 | children: List.generate(count, (index){ 24 | 25 | return AnimatedContainer( 26 | duration: cFastAnimationDuration, 27 | height: cDotSize, 28 | width: index == selectedIndex ? cSelectedDotSize : cDotSize, 29 | margin: const EdgeInsets.symmetric(horizontal: 2.5), 30 | decoration: BoxDecoration( 31 | borderRadius: BorderRadius.circular(cDotSize), 32 | color: index == selectedIndex ? cPrimaryColor : Color.alphaBlend(customTheme.extraLightColor, customTheme.backgroundColor), 33 | ), 34 | ); 35 | }), 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /lib/helpers/response_messages.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | class ResponseMessage{ 4 | List _messageList = []; 5 | 6 | final int? statusCode; 7 | final dynamic responseMessage; 8 | ResponseMessage({ 9 | required this.responseMessage, 10 | required this.statusCode 11 | }){ 12 | 13 | if(responseMessage is List){ 14 | _messageList = responseMessage 15 | .where((m) => m is String) 16 | .map((m) => (m as String).toLowerCase()) 17 | .toList(); 18 | } 19 | else if(responseMessage is String){ 20 | _messageList = [(responseMessage as String).toLowerCase()]; 21 | } 22 | } 23 | 24 | String get first => _messageList.firstOrNull ?? "Unexpected error"; 25 | 26 | bool contains(String key) => _messageList.any((m) => m.contains(key.toLowerCase())); 27 | 28 | String? get(String key) => _messageList.firstWhereOrNull((m) => m.contains(key.toLowerCase())); 29 | 30 | String? getIgnoring(String key, {required String ignore}) => 31 | _messageList.firstWhereOrNull((m) => m.contains(key.toLowerCase()) && !m.contains(ignore.toLowerCase())); 32 | 33 | bool checkFunction(bool Function(String) function) => _messageList.any((m) => function(m)); 34 | 35 | bool containsAnyStatusCodes(List statusCodes) => statusCodes.any((s) => statusCode == s); 36 | } -------------------------------------------------------------------------------- /lib/components/center_text_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/components/rounded_button.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class CenterTextIconButton extends StatelessWidget{ 6 | 7 | final EdgeInsets padding; 8 | final String text; 9 | final String iconAsset; 10 | final Function() onPressed; 11 | 12 | const CenterTextIconButton({ 13 | Key? key, 14 | this.padding = const EdgeInsets.symmetric(horizontal: 12.0), 15 | required this.text, 16 | required this.iconAsset, 17 | required this.onPressed 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final customTheme = Theme.of(context).customTheme; 23 | 24 | return RoundedButton( 25 | color: customTheme.contentBackgroundColor, 26 | child: Padding( 27 | padding: padding, 28 | child: NavigationToolbar( 29 | leading: SizedBox( 30 | height: 32.0, 31 | width: 32.0, 32 | child: Image.asset(iconAsset) 33 | ), 34 | middle: Text( 35 | text, 36 | style: customTheme.boldTextStyle.copyWith(fontSize: 13.5), 37 | maxLines: 1, 38 | ), 39 | ) 40 | ), 41 | onPressed: onPressed, 42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /lib/components/animated_chip.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class AnimatedChip extends StatelessWidget{ 6 | 7 | final String text; 8 | final Color? backgroundColor; 9 | final Color? textColor; 10 | final bool isLastItem; 11 | final Function()? onTap; 12 | 13 | const AnimatedChip({ 14 | Key? key, 15 | required this.text, 16 | this.backgroundColor, 17 | this.textColor, 18 | required this.isLastItem, 19 | this.onTap 20 | }) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final customTheme = Theme.of(context).customTheme; 25 | 26 | return GestureDetector( 27 | child: AnimatedContainer( 28 | duration: cFastAnimationDuration, 29 | padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), 30 | margin: EdgeInsets.only(right: isLastItem ? 0.0 : 8.0), 31 | 32 | decoration: BoxDecoration( 33 | color: backgroundColor ?? customTheme.contentBackgroundColor, 34 | borderRadius: const BorderRadius.all(Radius.circular(cBorderRadius)), 35 | ), 36 | 37 | child: Text( 38 | text, 39 | style: customTheme.smallTextStyle.copyWith(color: textColor), 40 | maxLines: 1, 41 | ) 42 | ), 43 | onTap: onTap, 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Manager App 2 | 3 | A Flutter app that allows you to create tasks and organize them into categories. It is built using the Bloc pattern, an implementation based on HydratedBloc that uses the Drifted database in separate Isolates and Firebase Cloud Messaging. 4 | 5 | ## 🎉 Features 6 | 7 | - Theme mode (Dark & Light) 8 | - Internationalization (English & Spanish) 9 | - Background synchronization 10 | - Offline mode available 11 | - Local notifications with history 12 | - Animations and transitions 13 | - and much more! 14 | 15 |
16 | 17 | ![4-min](https://user-images.githubusercontent.com/44307990/166523402-536dabb0-7d6e-47c1-bc7f-146862d2cde7.png) 18 | 19 | ![10](https://user-images.githubusercontent.com/44307990/166510124-6a0d287d-8efc-4b21-9270-c8a09661e974.png) 20 | 21 | ![9](https://user-images.githubusercontent.com/44307990/166509327-6caed1f8-5b62-46dc-beab-05bb6ceee9c5.png) 22 | 23 | ![11](https://user-images.githubusercontent.com/44307990/166521069-36121c4b-7b8a-4f14-add1-0c449d34154c.png) 24 | 25 | ![2](https://user-images.githubusercontent.com/44307990/166523735-212cd549-8c7c-4ede-b746-33ca62fe7fba.png) 26 | 27 |
28 | 29 | ## 🙇 Attributions and acknowledgments 30 | 31 | - Mockup images designed by [Freepik](https://www.freepik.com/) 32 | - SVG illustrations designed by [unDraw](https://undraw.co/) 33 | - Icon pack designed by [Icons8](https://icons8.com/) 34 | - Thanks to [Yusuf Rawat](https://github.com/Yusuf007R) for helping me and developing the specific backend and API 35 | -------------------------------------------------------------------------------- /lib/blocs/task_bloc/task_state.dart: -------------------------------------------------------------------------------- 1 | part of 'task_bloc.dart'; 2 | 3 | @JsonSerializable() 4 | class TaskState { 5 | 6 | final bool isLoading; 7 | final String? userId; 8 | final SyncStatus syncStatus; 9 | final List tasks; 10 | final List deletedTasks; 11 | final Map failedTasks; 12 | 13 | TaskState({ 14 | required this.isLoading, 15 | required this.userId, 16 | required this.syncStatus, 17 | required this.tasks, 18 | required this.deletedTasks, 19 | required this.failedTasks 20 | }); 21 | 22 | static TaskState get initial => TaskState( 23 | isLoading: true, 24 | userId: null, 25 | syncStatus: SyncStatus.idle, 26 | tasks: [], 27 | deletedTasks: [], 28 | failedTasks: {} 29 | ); 30 | 31 | TaskState copyWith({ 32 | bool? isLoading, 33 | String? userId, 34 | SyncStatus? syncStatus, 35 | List? tasks, 36 | List? deletedTasks, 37 | Map? failedTasks 38 | }){ 39 | return TaskState( 40 | isLoading: isLoading ?? this.isLoading, 41 | userId: userId ?? this.userId, 42 | syncStatus: syncStatus ?? SyncStatus.pending, 43 | tasks: tasks ?? this.tasks, 44 | deletedTasks: deletedTasks ?? this.deletedTasks, 45 | failedTasks: failedTasks ?? this.failedTasks 46 | ); 47 | } 48 | 49 | factory TaskState.fromJson(Map json) => _$TaskStateFromJson(json); 50 | Map toJson() => _$TaskStateToJson(this); 51 | } -------------------------------------------------------------------------------- /lib/blocs/calendar_bloc/calendar_state.dart: -------------------------------------------------------------------------------- 1 | part of 'calendar_bloc.dart'; 2 | 3 | class CalendarState { 4 | 5 | final bool isLoading; 6 | final DateTime selectedMonth; 7 | final List months; 8 | final DateTime selectedDay; 9 | final List days; 10 | final List items; 11 | 12 | CalendarState({ 13 | required this.isLoading, 14 | required this.selectedMonth, 15 | required this.months, 16 | required this.selectedDay, 17 | required this.days, 18 | required this.items 19 | }); 20 | 21 | static CalendarState get initial { 22 | final now = DateTime.now().ignoreTime; 23 | final start = DateTime(now.year - 1, now.month); 24 | final end = DateTime(now.year + 1, now.month); 25 | 26 | return CalendarState( 27 | isLoading: true, 28 | selectedMonth: now, 29 | months: start.monthsBefore(end), 30 | selectedDay: now, 31 | days: now.listDaysInMonth, 32 | items: [] 33 | ); 34 | } 35 | 36 | CalendarState copyWith({ 37 | bool? isLoading, 38 | DateTime? selectedMonth, 39 | DateTime? selectedDay, 40 | List? items 41 | }){ 42 | return CalendarState( 43 | isLoading: isLoading ?? this.isLoading, 44 | selectedMonth: selectedMonth ?? this.selectedMonth, 45 | months: months, 46 | selectedDay: selectedDay ?? this.selectedDay, 47 | days: selectedDay != null ? selectedDay.listDaysInMonth : days, 48 | items: items ?? this.items 49 | ); 50 | } 51 | } -------------------------------------------------------------------------------- /lib/screens/main_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/components/main/bottom_navigation_bar.dart'; 4 | import 'package:task_manager/constants.dart'; 5 | import 'package:task_manager/router/router.gr.dart'; 6 | import 'package:task_manager/theme/theme.dart'; 7 | 8 | class MainScreen extends StatelessWidget{ 9 | const MainScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final customTheme = Theme.of(context).customTheme; 14 | 15 | return AutoTabsScaffold( 16 | backgroundColor: customTheme.backgroundColor, 17 | 18 | animationDuration: cTransitionDuration, 19 | builder: (context, child, animation) { 20 | return FadeTransition( 21 | opacity: animation, 22 | child: SafeArea( 23 | child: child 24 | ) 25 | ); 26 | }, 27 | 28 | homeIndex: 0, 29 | routes: const [ 30 | HomeRoute(), 31 | CalendarRoute(), 32 | NotificationsRoute(), 33 | SettingsRoute() 34 | ], 35 | bottomNavigationBuilder: (_, tabsRouter) { 36 | 37 | return MyBottomNavigationBar( 38 | tabsRouter: tabsRouter, 39 | icons: const [ 40 | Icons.home_rounded, 41 | Icons.event_note_rounded, 42 | Icons.notifications_rounded, 43 | Icons.settings_rounded, 44 | ], 45 | ); 46 | }, 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /lib/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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options -------------------------------------------------------------------------------- /lib/models/active_session.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'active_session.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ActiveSession _$ActiveSessionFromJson(Map json) => 10 | ActiveSession( 11 | id: json['id'] as int, 12 | token: json['token'] as String, 13 | lastTimeOfUse: 14 | const DateTimeSerializer().fromJson(json['lastTimeOfUse'] as String), 15 | ipAddress: json['ipAddress'] as String, 16 | createdAt: 17 | const DateTimeSerializer().fromJson(json['createdAt'] as String), 18 | geoLocation: json['geoLocation'] == null 19 | ? null 20 | : GeoLocation.fromJson(json['geoLocation'] as Map), 21 | deviceName: json['deviceName'] as String, 22 | isThisDevice: json['isThisDevice'] as bool? ?? false, 23 | ); 24 | 25 | Map _$ActiveSessionToJson(ActiveSession instance) => 26 | { 27 | 'id': instance.id, 28 | 'token': instance.token, 29 | 'lastTimeOfUse': 30 | const DateTimeSerializer().toJson(instance.lastTimeOfUse), 31 | 'ipAddress': instance.ipAddress, 32 | 'createdAt': const DateTimeSerializer().toJson(instance.createdAt), 33 | 'geoLocation': instance.geoLocation, 34 | 'deviceName': instance.deviceName, 35 | 'isThisDevice': instance.isThisDevice, 36 | }; 37 | -------------------------------------------------------------------------------- /lib/models/task.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Task _$TaskFromJson(Map json) => Task( 10 | id: json['id'] as String, 11 | categoryId: json['categoryId'] as String?, 12 | title: json['title'] as String, 13 | description: json['description'] as String, 14 | date: const DateTimeSerializer().fromJson(json['date'] as String), 15 | isCompleted: json['isCompleted'] as bool? ?? false, 16 | createdAt: 17 | const DateTimeSerializer().fromJson(json['createdAt'] as String), 18 | updatedAt: 19 | const DateTimeSerializer().fromJson(json['updatedAt'] as String), 20 | deletedAt: const NullableDateTimeSerializer() 21 | .fromJson(json['deletedAt'] as String?), 22 | ); 23 | 24 | Map _$TaskToJson(Task instance) => { 25 | 'id': instance.id, 26 | 'categoryId': instance.categoryId, 27 | 'title': instance.title, 28 | 'description': instance.description, 29 | 'date': const DateTimeSerializer().toJson(instance.date), 30 | 'isCompleted': instance.isCompleted, 31 | 'createdAt': const DateTimeSerializer().toJson(instance.createdAt), 32 | 'updatedAt': const DateTimeSerializer().toJson(instance.updatedAt), 33 | 'deletedAt': 34 | const NullableDateTimeSerializer().toJson(instance.deletedAt), 35 | }; 36 | -------------------------------------------------------------------------------- /lib/helpers/response_errors.dart: -------------------------------------------------------------------------------- 1 | import 'package:connectivity_plus/connectivity_plus.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:task_manager/helpers/response_messages.dart'; 4 | import 'package:task_manager/services/dialog_service.dart'; 5 | import 'package:task_manager/services/locator_service.dart'; 6 | 7 | abstract class ResponseError{ 8 | 9 | static Future validate( 10 | Object error, 11 | List? ignoreKeys, 12 | { 13 | bool Function(String)? ignoreFunction, 14 | List? ignoreStatusCodes 15 | } 16 | ) async{ 17 | 18 | final connectivityResult = await Connectivity().checkConnectivity(); 19 | if(connectivityResult == ConnectivityResult.none){ 20 | try{ 21 | locator().showNoInternetConnectionDialog(); 22 | } 23 | catch(_){} 24 | return null; 25 | } 26 | 27 | if(error is DioError){ 28 | try{ 29 | final responseMessages = ResponseMessage( 30 | statusCode: error.response?.statusCode, 31 | responseMessage: error.response?.data["message"] 32 | ); 33 | 34 | if(ignoreKeys != null && ignoreKeys.any((key) => responseMessages.contains(key))) return responseMessages; 35 | if(ignoreFunction != null && responseMessages.checkFunction(ignoreFunction)) return responseMessages; 36 | if(ignoreStatusCodes != null && responseMessages.containsAnyStatusCodes(ignoreStatusCodes)) return responseMessages; 37 | } 38 | catch(_){} 39 | } 40 | 41 | try{ 42 | locator().showSomethingWentWrongDialog(); 43 | } 44 | catch(_){} 45 | return null; 46 | } 47 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/blocs/task_bloc/task_bloc.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task_bloc.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaskState _$TaskStateFromJson(Map json) => TaskState( 10 | isLoading: json['isLoading'] as bool, 11 | userId: json['userId'] as String?, 12 | syncStatus: $enumDecode(_$SyncStatusEnumMap, json['syncStatus']), 13 | tasks: (json['tasks'] as List) 14 | .map((e) => Task.fromJson(e as Map)) 15 | .toList(), 16 | deletedTasks: (json['deletedTasks'] as List) 17 | .map((e) => Task.fromJson(e as Map)) 18 | .toList(), 19 | failedTasks: (json['failedTasks'] as Map).map( 20 | (k, e) => MapEntry(k, $enumDecode(_$SyncErrorTypeEnumMap, e)), 21 | ), 22 | ); 23 | 24 | Map _$TaskStateToJson(TaskState instance) => { 25 | 'isLoading': instance.isLoading, 26 | 'userId': instance.userId, 27 | 'syncStatus': _$SyncStatusEnumMap[instance.syncStatus], 28 | 'tasks': instance.tasks, 29 | 'deletedTasks': instance.deletedTasks, 30 | 'failedTasks': instance.failedTasks 31 | .map((k, e) => MapEntry(k, _$SyncErrorTypeEnumMap[e])), 32 | }; 33 | 34 | const _$SyncStatusEnumMap = { 35 | SyncStatus.idle: 'idle', 36 | SyncStatus.pending: 'pending', 37 | }; 38 | 39 | const _$SyncErrorTypeEnumMap = { 40 | SyncErrorType.duplicatedId: 'duplicatedId', 41 | SyncErrorType.blacklist: 'blacklist', 42 | }; 43 | -------------------------------------------------------------------------------- /lib/models/notification_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:task_manager/l10n/l10n.dart'; 4 | import 'package:task_manager/models/serializers/color_serializer.dart'; 5 | 6 | part 'notification_type.freezed.dart'; 7 | part 'notification_type.g.dart'; 8 | 9 | @freezed 10 | class NotificationType with _$NotificationType{ 11 | 12 | const factory NotificationType.general({ 13 | @ColorSerializer() 14 | @Default(Color(0xFF008FFD)) 15 | Color color 16 | }) = GeneralNotification; 17 | 18 | const factory NotificationType.reminder({ 19 | @ColorSerializer() 20 | @Default(Color(0xFF5C5D9D)) 21 | Color color 22 | }) = ReminderNotification; 23 | 24 | const factory NotificationType.security({ 25 | @ColorSerializer() 26 | @Default(Color(0xFFFF8700)) 27 | Color color 28 | }) = SecurityNotification; 29 | 30 | const factory NotificationType.advertisement({ 31 | @ColorSerializer() 32 | @Default(Color(0xFFFFBF47)) 33 | Color color 34 | }) = AdvertisementNotification; 35 | 36 | factory NotificationType.fromJson(Map json) => _$NotificationTypeFromJson(json); 37 | } 38 | 39 | extension NotificationTypeExtension on NotificationType? { 40 | String nameLocalization(BuildContext context, {bool isPlural = false}) { 41 | final countValue = isPlural ? 2 : 1; 42 | if(this == null) return context.l10n.enum_notificationType_all; 43 | if(this is ReminderNotification) return context.l10n.enum_notificationType_reminder(countValue); 44 | if(this is AdvertisementNotification) return context.l10n.enum_notificationType_advertisement(countValue); 45 | return "Unknown"; 46 | } 47 | } -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | task_manager 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | UIStatusBarHidden 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/blocs/category_bloc/category_state.dart: -------------------------------------------------------------------------------- 1 | part of 'category_bloc.dart'; 2 | 3 | @JsonSerializable() 4 | class CategoryState{ 5 | 6 | final bool isLoading; 7 | final String? userId; 8 | final SyncStatus syncStatus; 9 | final List categories; 10 | final List deletedCategories; 11 | final Map failedCategories; 12 | 13 | CategoryState({ 14 | required this.isLoading, 15 | required this.userId, 16 | required this.syncStatus, 17 | required this.categories, 18 | required this.deletedCategories, 19 | required this.failedCategories 20 | }); 21 | 22 | static CategoryState get initial => CategoryState( 23 | isLoading: true, 24 | userId: null, 25 | syncStatus: SyncStatus.idle, 26 | categories: [ 27 | Category.create( 28 | isGeneral: true, 29 | name: "General", 30 | color: Colors.grey.withOpacity(0.65) 31 | ) 32 | ], 33 | deletedCategories: [], 34 | failedCategories: {} 35 | ); 36 | 37 | CategoryState copyWith({ 38 | bool? isLoading, 39 | String? userId, 40 | SyncStatus? syncStatus, 41 | List? categories, 42 | List? deletedCategories, 43 | Map? failedCategories 44 | }){ 45 | return CategoryState( 46 | isLoading: isLoading ?? this.isLoading, 47 | userId: userId ?? this.userId, 48 | syncStatus: syncStatus ?? SyncStatus.pending, 49 | categories: categories ?? this.categories, 50 | deletedCategories: deletedCategories ?? this.deletedCategories, 51 | failedCategories: failedCategories ?? this.failedCategories 52 | ); 53 | } 54 | 55 | factory CategoryState.fromJson(Map json) => _$CategoryStateFromJson(json); 56 | Map toJson() => _$CategoryStateToJson(this); 57 | } -------------------------------------------------------------------------------- /lib/bottom_sheets/date_picker_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/bottom_sheets/modal_bottom_sheet.dart'; 3 | import 'package:task_manager/components/rounded_button.dart'; 4 | import 'package:task_manager/l10n/l10n.dart'; 5 | 6 | import '../constants.dart'; 7 | 8 | class DatePickerBottomSheet{ 9 | 10 | final BuildContext context; 11 | final DateTime initialDate; 12 | final ValueChanged onDateChanged; 13 | 14 | DatePickerBottomSheet( 15 | this.context, 16 | { 17 | required this.initialDate, 18 | required this.onDateChanged 19 | } 20 | ); 21 | 22 | void show(){ 23 | ModalBottomSheet( 24 | title: context.l10n.bottomSheet_selectDate, 25 | context: context, 26 | content: _DatePickerBottomSheet(this) 27 | ).show(); 28 | } 29 | } 30 | 31 | class _DatePickerBottomSheet extends StatelessWidget{ 32 | 33 | final DatePickerBottomSheet data; 34 | const _DatePickerBottomSheet(this.data); 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | 39 | return Padding( 40 | padding: const EdgeInsets.symmetric(horizontal: cPadding), 41 | child: Column( 42 | children: [ 43 | Padding( 44 | padding: const EdgeInsets.symmetric(vertical: cPadding), 45 | child: CalendarDatePicker( 46 | initialDate: data.initialDate, 47 | firstDate: DateTime(1969, 1, 1), 48 | lastDate: DateTime.now().add(const Duration(days: 365)), 49 | onDateChanged: data.onDateChanged 50 | ), 51 | ), 52 | RoundedTextButton( 53 | text: context.l10n.select_button, 54 | onPressed: () => Navigator.of(context).pop() 55 | ) 56 | ], 57 | ), 58 | ); 59 | } 60 | } -------------------------------------------------------------------------------- /lib/components/forms/outlined_form_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class OutlinedFormIconButton extends StatelessWidget{ 6 | 7 | final IconData icon; 8 | final String text; 9 | final Color? outlineColor; 10 | final bool expand; 11 | final Function() onPressed; 12 | 13 | const OutlinedFormIconButton({ 14 | Key? key, 15 | required this.icon, 16 | required this.text, 17 | this.outlineColor, 18 | this.expand = false, 19 | required this.onPressed 20 | }) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final customTheme = Theme.of(context).customTheme; 25 | 26 | return OutlinedButton( 27 | onPressed: onPressed, 28 | child: Row( 29 | children: [ 30 | Icon( 31 | icon, 32 | size: 22.0, 33 | color: customTheme.lightColor 34 | ), 35 | const SizedBox(width: 4.0), 36 | 37 | Expanded( 38 | flex: expand ? 1 : 0, 39 | child: Text( 40 | text, 41 | style: customTheme.smallTextStyle, 42 | textAlign: TextAlign.center, 43 | maxLines: 1 44 | ), 45 | ) 46 | ], 47 | ), 48 | style: OutlinedButton.styleFrom( 49 | padding: const EdgeInsets.symmetric( 50 | horizontal: 16.0, 51 | vertical: 12.0 52 | ), 53 | side: BorderSide( 54 | width: 1.5, 55 | color: outlineColor ?? customTheme.extraLightColor 56 | ), 57 | shape: RoundedRectangleBorder( 58 | borderRadius: BorderRadius.circular(cBorderRadius), 59 | ), 60 | ), 61 | ); 62 | } 63 | } -------------------------------------------------------------------------------- /lib/blocs/category_bloc/category_bloc.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'category_bloc.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CategoryState _$CategoryStateFromJson(Map json) => 10 | CategoryState( 11 | isLoading: json['isLoading'] as bool, 12 | userId: json['userId'] as String?, 13 | syncStatus: $enumDecode(_$SyncStatusEnumMap, json['syncStatus']), 14 | categories: (json['categories'] as List) 15 | .map((e) => Category.fromJson(e as Map)) 16 | .toList(), 17 | deletedCategories: (json['deletedCategories'] as List) 18 | .map((e) => Category.fromJson(e as Map)) 19 | .toList(), 20 | failedCategories: (json['failedCategories'] as Map).map( 21 | (k, e) => MapEntry(k, $enumDecode(_$SyncErrorTypeEnumMap, e)), 22 | ), 23 | ); 24 | 25 | Map _$CategoryStateToJson(CategoryState instance) => 26 | { 27 | 'isLoading': instance.isLoading, 28 | 'userId': instance.userId, 29 | 'syncStatus': _$SyncStatusEnumMap[instance.syncStatus], 30 | 'categories': instance.categories, 31 | 'deletedCategories': instance.deletedCategories, 32 | 'failedCategories': instance.failedCategories 33 | .map((k, e) => MapEntry(k, _$SyncErrorTypeEnumMap[e])), 34 | }; 35 | 36 | const _$SyncStatusEnumMap = { 37 | SyncStatus.idle: 'idle', 38 | SyncStatus.pending: 'pending', 39 | }; 40 | 41 | const _$SyncErrorTypeEnumMap = { 42 | SyncErrorType.duplicatedId: 'duplicatedId', 43 | SyncErrorType.blacklist: 'blacklist', 44 | }; 45 | -------------------------------------------------------------------------------- /lib/components/main/center_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class CenterAppBar extends StatelessWidget { 6 | 7 | final Widget center; 8 | final Widget? leading; 9 | final bool automaticallyImplyLeading; 10 | final List? actions; 11 | 12 | const CenterAppBar({ 13 | Key? key, 14 | required this.center, 15 | this.leading, 16 | this.automaticallyImplyLeading = true, 17 | this.actions 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final customTheme = Theme.of(context).customTheme; 23 | 24 | final ModalRoute? parentRoute = ModalRoute.of(context); 25 | final bool canPop = parentRoute?.canPop ?? false; 26 | 27 | return Padding( 28 | padding: const EdgeInsets.all(16.0), 29 | child: Row( 30 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 31 | children: [ 32 | Expanded( 33 | child: Row( 34 | children: [ 35 | if(leading != null) leading! 36 | else if(automaticallyImplyLeading && canPop) IconButton( 37 | color: customTheme.lightColor, 38 | icon: const Icon(Icons.navigate_before_rounded,), 39 | splashRadius: cSplashRadius, 40 | onPressed: () => Navigator.of(context).maybePop() 41 | ), 42 | ], 43 | ), 44 | ), 45 | 46 | center, 47 | 48 | Expanded( 49 | child: Row( 50 | mainAxisAlignment: MainAxisAlignment.end, 51 | children: actions != null ? actions! : [] 52 | ), 53 | ) 54 | ], 55 | ), 56 | ); 57 | } 58 | } -------------------------------------------------------------------------------- /lib/messaging/types/background_auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_messaging/firebase_messaging.dart'; 2 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 3 | import 'package:task_manager/blocs/drifted_bloc/drifted_bloc.dart'; 4 | import 'package:task_manager/blocs/drifted_bloc/drifted_storage.dart'; 5 | import 'package:task_manager/blocs/notifications_cubit/notifications_cubit.dart'; 6 | import 'package:task_manager/blocs/settings_cubit/settings_cubit.dart'; 7 | import 'package:task_manager/messaging/data_notifications.dart'; 8 | import 'package:task_manager/messaging/messaging_helper.dart'; 9 | import 'package:task_manager/services/notification_service.dart'; 10 | 11 | abstract class BackgroundAuth{ 12 | 13 | static void backgroundAuthHandler( 14 | RemoteMessage message, 15 | DataNotificationType dataNotificationType, 16 | ) async{ 17 | 18 | try{ 19 | final storage = await DriftedStorage.build(); 20 | DriftedBlocOverrides.runZoned( 21 | () async { 22 | 23 | final settingsCubit = SettingsCubit(); 24 | 25 | final notificationService = NotificationService(); 26 | final notificationsCubit = NotificationsCubit( 27 | notificationService: notificationService, 28 | settingsCubit: settingsCubit 29 | ); 30 | 31 | final authBloc = AuthBloc(notificationsCubit: notificationsCubit); 32 | authBloc.add(DataNotificationReceived(dataNotificationType)); 33 | 34 | MessagingHelper.autoCloseBlocs( 35 | blocs: [ 36 | settingsCubit, 37 | authBloc, 38 | notificationsCubit 39 | ], 40 | onClosed: (){ 41 | notificationService.close(); 42 | } 43 | ); 44 | }, 45 | storage: storage 46 | ); 47 | } 48 | catch(_){} 49 | } 50 | } -------------------------------------------------------------------------------- /lib/components/calendar/calendar_group_hour.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/helpers/date_time_helper.dart'; 4 | import 'package:task_manager/theme/theme.dart'; 5 | 6 | class CalendarGroupHour extends StatelessWidget{ 7 | 8 | final DateTime dateTime; 9 | const CalendarGroupHour({ 10 | Key? key, 11 | required this.dateTime, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | 17 | return Padding( 18 | padding: const EdgeInsets.symmetric(vertical: cListItemSpace), 19 | child: IntrinsicHeight( 20 | child: Row( 21 | crossAxisAlignment: CrossAxisAlignment.center, 22 | children: [ 23 | Stack( 24 | children: [ 25 | CalendarGroupHourText(text: dateTime.format(context, "HH:mm")), 26 | const CalendarGroupHourText(text: "12:00 ", visible: false) 27 | ], 28 | ), 29 | 30 | const Expanded( 31 | child: Divider() 32 | ) 33 | ], 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | 40 | class CalendarGroupHourText extends StatelessWidget{ 41 | final String text; 42 | final bool visible; 43 | 44 | const CalendarGroupHourText({ 45 | Key? key, 46 | required this.text, 47 | this.visible = true 48 | }) : super(key: key); 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | final customTheme = Theme.of(context).customTheme; 53 | 54 | return Opacity( 55 | opacity: visible ? 1 : 0, 56 | child: Padding( 57 | padding: const EdgeInsets.only(right: 12.0), 58 | child: Text( 59 | text, 60 | style: customTheme.lightTextStyle, 61 | maxLines: 1 62 | ), 63 | ), 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /lib/router/wrappers/main_router_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 5 | import 'package:task_manager/blocs/category_bloc/category_bloc.dart'; 6 | import 'package:task_manager/blocs/notifications_cubit/notifications_cubit.dart'; 7 | import 'package:task_manager/blocs/sync_bloc/sync_bloc.dart'; 8 | import 'package:task_manager/blocs/task_bloc/task_bloc.dart'; 9 | import 'package:task_manager/repositories/sync_repository.dart'; 10 | 11 | class MainRouteWrapper extends StatelessWidget { 12 | const MainRouteWrapper({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return RepositoryProvider( 17 | create: (context) => SyncRepository( 18 | base: context.read().baseRepository 19 | ), 20 | child: MultiBlocProvider( 21 | providers: [ 22 | BlocProvider(create: (context) => TaskBloc( 23 | inBackground: false, 24 | authBloc: context.read(), 25 | notificationsCubit: context.read() 26 | )), 27 | 28 | BlocProvider(create: (context) => CategoryBloc( 29 | inBackground: false, 30 | authBloc: context.read(), 31 | taskBloc: context.read() 32 | )), 33 | 34 | BlocProvider(create: (context) => SyncBloc( 35 | inBackground: false, 36 | authBloc: context.read(), 37 | taskBloc: context.read(), 38 | categoryBloc: context.read(), 39 | syncRepository: context.read(), 40 | ), lazy: false), 41 | ], 42 | child: const AutoRouter(), 43 | ), 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /lib/services/notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:awesome_notifications/awesome_notifications.dart'; 4 | import 'package:task_manager/helpers/string_helper.dart'; 5 | import 'package:task_manager/models/notification_type.dart'; 6 | 7 | class NotificationService{ 8 | 9 | late Map channels = { 10 | for (var type in [ 11 | const NotificationType.general(), 12 | const NotificationType.reminder(), 13 | const NotificationType.security(), 14 | const NotificationType.advertisement() 15 | ]) type : _channelFromType(type) 16 | }; 17 | 18 | NotificationChannel _channelFromType(NotificationType type){ 19 | final name = (type.toJson()["runtimeType"] as String?) ?? "basic"; 20 | return NotificationChannel( 21 | channelKey: "$name channel".toLowerSnakeCase, 22 | channelName: "${name.capitalize} notifications", 23 | channelDescription: "Description not provided", 24 | defaultColor: type.color 25 | ); 26 | } 27 | 28 | StreamSubscription? _displayedSubscription; 29 | final _displayedController = StreamController.broadcast(); 30 | Stream get displayedStream => _displayedController.stream; 31 | 32 | NotificationService(){ 33 | _init(); 34 | } 35 | 36 | void _init() async { 37 | try{ 38 | await AwesomeNotifications().initialize( 39 | null, 40 | channels.values.toSet().toList() 41 | ); 42 | 43 | _displayedSubscription = AwesomeNotifications().displayedStream.listen( 44 | (notification) => _displayedController.add(notification) 45 | ); 46 | } 47 | catch(_){} 48 | } 49 | 50 | void close() async { 51 | try{ 52 | await _displayedSubscription?.cancel(); 53 | await _displayedController.close(); 54 | } 55 | catch(_){} 56 | } 57 | } -------------------------------------------------------------------------------- /lib/models/auth_credentials.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:jwt_decoder/jwt_decoder.dart'; 3 | 4 | part 'auth_credentials.g.dart'; 5 | 6 | @JsonSerializable() 7 | class AuthCredentials{ 8 | final String refreshToken; 9 | final String accessToken; 10 | final String passwordToken; 11 | 12 | const AuthCredentials({ 13 | this.refreshToken = "", 14 | this.accessToken = "", 15 | this.passwordToken = "" 16 | }); 17 | 18 | static const empty = AuthCredentials(); 19 | 20 | bool get isEmpty => 21 | refreshToken == "" 22 | && accessToken == "" 23 | && passwordToken == ""; 24 | 25 | bool get isNotEmpty => !isEmpty; 26 | 27 | bool get isVerified{ 28 | if(isNotEmpty){ 29 | try{ 30 | return JwtDecoder.decode(accessToken)["verified"]; 31 | } 32 | catch(_){} 33 | } 34 | return false; 35 | } 36 | 37 | factory AuthCredentials.fromJson(Map json) => _$AuthCredentialsFromJson(json); 38 | Map toJson() => _$AuthCredentialsToJson(this); 39 | 40 | AuthCredentials copyWith({ 41 | String? refreshToken, 42 | String? accessToken, 43 | String? passwordToken 44 | }){ 45 | return AuthCredentials( 46 | refreshToken: refreshToken ?? this.refreshToken, 47 | accessToken: accessToken ?? this.accessToken, 48 | passwordToken: passwordToken ?? this.passwordToken 49 | ); 50 | } 51 | } 52 | 53 | extension AuthCredentialsExtension on AuthCredentials{ 54 | AuthCredentials merge(AuthCredentials? other){ 55 | return other != null ? AuthCredentials( 56 | refreshToken: other.refreshToken.isNotEmpty ? other.refreshToken : refreshToken, 57 | accessToken: other.accessToken.isNotEmpty ? other.accessToken : accessToken, 58 | passwordToken: other.passwordToken.isNotEmpty ? other.passwordToken : passwordToken, 59 | ) : this; 60 | } 61 | } -------------------------------------------------------------------------------- /lib/blocs/drifted_bloc/drifted_isolate.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | import 'dart:ui'; 4 | 5 | import 'package:drift/isolate.dart'; 6 | import 'package:drift/drift.dart'; 7 | import 'package:drift/native.dart'; 8 | import 'package:path/path.dart' as p; 9 | import 'package:path_provider/path_provider.dart'; 10 | import 'package:task_manager/blocs/drifted_bloc/drifted_database.dart'; 11 | 12 | abstract class DriftedIsolate{ 13 | 14 | static Future _createDriftIsolate() async { 15 | final directory = await getApplicationDocumentsDirectory(); 16 | final path = p.join(directory.path, 'db.sqlite'); 17 | 18 | final executor = NativeDatabase(File(path)); 19 | final driftIsolate = DriftIsolate.inCurrent( 20 | () => DatabaseConnection.fromExecutor(executor), 21 | ); 22 | 23 | return driftIsolate; 24 | } 25 | 26 | static Future createOrGetDatabase() async{ 27 | final databasePort = IsolateNameServer.lookupPortByName( 28 | DriftedDatabase.databasePortName 29 | ); 30 | 31 | SendPort sendPort; 32 | DriftedDatabase database = DriftedDatabase.connect( 33 | await DriftIsolate.fromConnectPort( 34 | sendPort = databasePort ?? (await _createDriftIsolate()).connectPort 35 | ).connect() 36 | ); 37 | 38 | await (database.executor.ensureOpen(database.attachedDatabase)) 39 | .timeout(const Duration(seconds: 1), onTimeout: () async{ 40 | 41 | IsolateNameServer.removePortNameMapping(DriftedDatabase.databasePortName); 42 | await database.close(); 43 | 44 | database = DriftedDatabase.connect(await DriftIsolate.fromConnectPort( 45 | sendPort = (await _createDriftIsolate()).connectPort 46 | ).connect()); 47 | 48 | return false; 49 | } 50 | ); 51 | 52 | IsolateNameServer.registerPortWithName( 53 | sendPort, DriftedDatabase.databasePortName 54 | ); 55 | return database; 56 | } 57 | } -------------------------------------------------------------------------------- /lib/blocs/drifted_bloc/drifted_database.dart: -------------------------------------------------------------------------------- 1 | import 'package:drift/drift.dart'; 2 | import 'package:task_manager/blocs/drifted_bloc/drifted_converters.dart'; 3 | 4 | part 'drifted_database.g.dart'; 5 | 6 | @DataClassName("DriftedState") 7 | class DriftedStates extends Table { 8 | 9 | TextColumn get key => text()(); 10 | TextColumn get state => text().map(const StateConverter())(); 11 | IntColumn get updatedAt => integer().map(const DateTimeConverter())(); 12 | 13 | @override 14 | Set get primaryKey => {key}; 15 | } 16 | 17 | @DriftDatabase(tables: [DriftedStates]) 18 | class DriftedDatabase extends _$DriftedDatabase{ 19 | DriftedDatabase.connect(DatabaseConnection connection) : super.connect(connection); 20 | 21 | @override 22 | int get schemaVersion => 1; 23 | 24 | @override 25 | MigrationStrategy get migration { 26 | return MigrationStrategy( 27 | onCreate: (Migrator m) { 28 | return m.createAll(); 29 | }, 30 | ); 31 | } 32 | 33 | static String get databasePortName => "DriftedDatabasePort"; 34 | 35 | Future insertOrReplaceState(DriftedState state) async { 36 | try{ 37 | await into(driftedStates) 38 | .insert(state, mode: InsertMode.insertOrReplace); 39 | } 40 | catch(_) {} 41 | } 42 | 43 | Future?> getStates() async { 44 | try{ 45 | return select(driftedStates).get(); 46 | } 47 | catch(_) {} 48 | return null; 49 | } 50 | 51 | Future deleteState(String key) async { 52 | try{ 53 | await (delete(driftedStates) 54 | ..where((s) => s.key.equals(key)) 55 | ).go(); 56 | } 57 | catch(_) {} 58 | } 59 | 60 | Stream>? get watch { 61 | try{ 62 | return select(driftedStates).watch(); 63 | } 64 | catch(_) {} 65 | return null; 66 | } 67 | 68 | Future clear() async { 69 | try{ 70 | await delete(driftedStates).go(); 71 | } 72 | catch(_) {} 73 | } 74 | } -------------------------------------------------------------------------------- /lib/components/lists/snap_bounce_scroll_physics.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | 4 | // https://github.com/flutter/flutter/issues/41472 5 | 6 | class SnapBounceScrollPhysics extends BouncingScrollPhysics { 7 | final double itemWidth; 8 | 9 | const SnapBounceScrollPhysics({ 10 | required this.itemWidth, 11 | ScrollPhysics? parent, 12 | }) : super(parent: parent); 13 | 14 | @override 15 | SnapBounceScrollPhysics applyTo(ScrollPhysics? ancestor) => SnapBounceScrollPhysics( 16 | parent: buildParent(ancestor), 17 | itemWidth: itemWidth, 18 | ); 19 | 20 | double _getItem(ScrollPosition position) => (position.pixels) / itemWidth; 21 | 22 | double _getPixels(ScrollPosition position, double item) => 23 | min(max(item * itemWidth, position.minScrollExtent), position.maxScrollExtent); 24 | 25 | double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) { 26 | double item = _getItem(position); 27 | item += velocity / 1000; 28 | return _getPixels(position, item.roundToDouble()); 29 | } 30 | 31 | @override 32 | Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { 33 | // If we're out of range and not headed back in range, defer to the parent 34 | // ballistics, which should put us back in range at a page boundary. 35 | if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || 36 | (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) { 37 | return super.createBallisticSimulation(position, velocity); 38 | } 39 | final Tolerance tolerance = this.tolerance; 40 | final double target = _getTargetPixels(position as ScrollPosition, tolerance, velocity); 41 | if (target != position.pixels) { 42 | return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); 43 | } 44 | return null; 45 | } 46 | 47 | @override 48 | bool get allowImplicitScrolling => false; 49 | } -------------------------------------------------------------------------------- /lib/services/dialog_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/components/rounded_alert_dialog.dart'; 3 | import 'package:task_manager/components/rounded_button.dart'; 4 | import 'package:task_manager/l10n/l10n.dart'; 5 | 6 | class DialogService { 7 | 8 | GlobalKey? _navigatorKey; 9 | void init(GlobalKey key) => _navigatorKey = key; 10 | 11 | BuildContext? get _getCurrentContext => _navigatorKey?.currentContext; 12 | 13 | void showNoInternetConnectionDialog() { 14 | final context = _getCurrentContext; 15 | 16 | if(context != null){ 17 | RoundedAlertDialog( 18 | buildContext: context, 19 | svgImage: "assets/svg/stars.svg", 20 | title: context.l10n.alertDialog_noInternetConnection, 21 | description: context.l10n.alertDialog_noInternetConnection_description, 22 | actions: [ 23 | RoundedTextButton( 24 | expandWidth: false, 25 | textPadding: const EdgeInsets.symmetric(horizontal: 32.0), 26 | text: context.l10n.gotIt_button, 27 | onPressed: () => Navigator.of(context, rootNavigator: true).pop() 28 | ), 29 | ], 30 | ).show(); 31 | } 32 | } 33 | 34 | void showSomethingWentWrongDialog(){ 35 | final context = _getCurrentContext; 36 | 37 | if(context != null){ 38 | RoundedAlertDialog( 39 | buildContext: context, 40 | svgImage: "assets/svg/stars.svg", 41 | title: context.l10n.alertDialog_somethingWentWrong, 42 | description: context.l10n.alertDialog_somethingWentWrong_description, 43 | actions: [ 44 | RoundedTextButton( 45 | expandWidth: false, 46 | textPadding: const EdgeInsets.symmetric(horizontal: 32.0), 47 | text: context.l10n.gotIt_button, 48 | onPressed: () => Navigator.of(context, rootNavigator: true).pop() 49 | ), 50 | ], 51 | ).show(); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /lib/models/notification_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:awesome_notifications/awesome_notifications.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | import 'package:task_manager/models/notification_type.dart'; 4 | import 'package:collection/collection.dart'; 5 | 6 | part 'notification_data.g.dart'; 7 | 8 | @JsonSerializable() 9 | class NotificationData{ 10 | 11 | final int id; 12 | final String title; 13 | final String body; 14 | final DateTime createdAt; 15 | final DateTime? scheduledAt; 16 | final NotificationType type; 17 | 18 | const NotificationData({ 19 | required this.id, 20 | required this.title, 21 | required this.body, 22 | required this.createdAt, 23 | this.scheduledAt, 24 | required this.type 25 | }); 26 | 27 | static Future create({ 28 | required String title, 29 | required String body, 30 | DateTime? scheduledAt, 31 | required NotificationType type 32 | }) async { 33 | final scheduledNotifications = await AwesomeNotifications().listScheduledNotifications(); 34 | 35 | scheduledNotifications.sort((a, b) { 36 | final idA = a.content?.id; 37 | final idB = b.content?.id; 38 | 39 | if(idA != null && idB != null){ 40 | if(idA > idB) return 1; 41 | if(idA < idB) return -1; 42 | return 0; 43 | } 44 | 45 | if(idA == null && idB != null) return -1; 46 | if(idA != null && idB == null) return 1; 47 | return 0; 48 | }); 49 | 50 | final lastId = scheduledNotifications.lastOrNull?.content?.id; 51 | final id = lastId != null ? lastId + 1 : 0; 52 | 53 | return NotificationData( 54 | id: id, 55 | title: title, 56 | body: body, 57 | createdAt: DateTime.now(), 58 | type: type, 59 | scheduledAt: scheduledAt 60 | ); 61 | } 62 | 63 | factory NotificationData.fromJson(Map json) => _$NotificationDataFromJson(json); 64 | Map toJson() => _$NotificationDataToJson(this); 65 | } -------------------------------------------------------------------------------- /lib/repositories/task_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:task_manager/repositories/base_repository.dart'; 2 | 3 | class TaskRepository{ 4 | 5 | final BaseRepository base; 6 | TaskRepository({required this.base}); 7 | 8 | /*Future?> getTasks() async { 9 | try{ 10 | final response = await _dio.get( 11 | "/", 12 | options: Options(headers: {"Authorization": "Bearer " + authBloc.state.credentials.accessToken}) 13 | ); 14 | return List.from(response.data 15 | .map((task) => Task.fromJson(task)) 16 | .where(((task) => task.id != null)) 17 | ); 18 | } 19 | catch (error){ 20 | onResponseError(error: error); 21 | } 22 | } 23 | 24 | Future createTask(Task task) async { 25 | try{ 26 | final response = await _dio.post( 27 | "/", 28 | data: jsonEncode(task), 29 | options: Options(headers: {"Authorization": "Bearer " + authBloc.state.credentials.accessToken}) 30 | ); 31 | return Task.fromJson(response.data); 32 | } 33 | catch (error){ 34 | onResponseError(error: error); 35 | } 36 | } 37 | 38 | Future updateTask(Task task) async { 39 | try{ 40 | final response = await _dio.patch( 41 | "/${task.id}", 42 | data: jsonEncode(task), 43 | options: Options(headers: {"Authorization": "Bearer " + authBloc.state.credentials.accessToken}) 44 | ); 45 | return Task.fromJson(response.data); 46 | } 47 | catch (error){ 48 | onResponseError(error: error); 49 | } 50 | } 51 | 52 | Future deleteTask(Task task) async { 53 | try{ 54 | final response = await _dio.delete( 55 | "/${task.id}", 56 | data: jsonEncode(task), 57 | options: Options(headers: {"Authorization": "Bearer " + authBloc.state.credentials.accessToken}) 58 | ); 59 | return Task.fromJson(response.data); 60 | } 61 | catch (error){ 62 | onResponseError(error: error); 63 | } 64 | }*/ 65 | } -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: lines_longer_than_80_chars 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | // ignore: missing_enum_constant_in_switch 26 | switch (defaultTargetPlatform) { 27 | case TargetPlatform.android: 28 | return android; 29 | case TargetPlatform.iOS: 30 | throw UnsupportedError( 31 | 'DefaultFirebaseOptions have not been configured for ios - ' 32 | 'you can reconfigure this by running the FlutterFire CLI again.', 33 | ); 34 | case TargetPlatform.macOS: 35 | throw UnsupportedError( 36 | 'DefaultFirebaseOptions have not been configured for macos - ' 37 | 'you can reconfigure this by running the FlutterFire CLI again.', 38 | ); 39 | } 40 | 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions are not supported for this platform.', 43 | ); 44 | } 45 | 46 | static const FirebaseOptions android = FirebaseOptions( 47 | apiKey: 'AIzaSyCPH9lXTMBXm1iDMXqZByWq2Vt6FsKDyIU', 48 | appId: '1:209326263434:android:2fe70e2f07127221bc7173', 49 | messagingSenderId: '209326263434', 50 | projectId: 'task-manager-1d40c', 51 | storageBucket: 'task-manager-1d40c.appspot.com', 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/components/dot_tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tab_indicator_styler/tab_indicator_styler.dart'; 3 | import 'package:task_manager/constants.dart'; 4 | import 'package:task_manager/theme/theme.dart'; 5 | 6 | class DotTabBar extends StatelessWidget { 7 | 8 | final TabController? controller; 9 | final List tabs; 10 | final Function(int)? onTap; 11 | final double? width; 12 | 13 | const DotTabBar({ 14 | Key? key, 15 | this.controller, 16 | required this.tabs, 17 | this.onTap, 18 | this.width 19 | }) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final customTheme = Theme.of(context).customTheme; 24 | 25 | final tabBar = Theme( 26 | data: Theme.of(context).copyWith( 27 | highlightColor: Colors.transparent, 28 | splashColor: Colors.transparent, 29 | hoverColor: Colors.transparent, 30 | ), 31 | child: TabBar( 32 | controller: controller, 33 | isScrollable: true, 34 | physics: BouncingScrollPhysics( 35 | parent: width != null ? const AlwaysScrollableScrollPhysics() : null 36 | ), 37 | 38 | indicatorSize: TabBarIndicatorSize.tab, 39 | indicatorWeight: 0.0, 40 | 41 | indicator: DotIndicator( 42 | color: cPrimaryColor, 43 | distanceFromCenter: 20.0 44 | ), 45 | 46 | padding: const EdgeInsets.symmetric(horizontal: cPadding), 47 | labelPadding: const EdgeInsets.only(right: 32.0), 48 | indicatorPadding: const EdgeInsets.only(right: 32.0), 49 | 50 | labelStyle: customTheme.textStyle, 51 | labelColor: customTheme.textColor, 52 | unselectedLabelColor: customTheme.lightTextColor, 53 | 54 | tabs: tabs, 55 | onTap: onTap 56 | ), 57 | ); 58 | 59 | return width != null ? SizedBox( 60 | width: width, 61 | child: tabBar, 62 | ) : Align( 63 | alignment: Alignment.centerLeft, 64 | child: tabBar, 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /lib/models/active_session.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:task_manager/l10n/l10n.dart'; 5 | import 'package:task_manager/models/geo_location.dart'; 6 | import 'package:task_manager/models/serializers/datetime_serializer.dart'; 7 | 8 | part 'active_session.g.dart'; 9 | 10 | @JsonSerializable() 11 | class ActiveSession extends Equatable{ 12 | 13 | final int id; 14 | final String token; 15 | @DateTimeSerializer() 16 | final DateTime lastTimeOfUse; 17 | final String ipAddress; 18 | @DateTimeSerializer() 19 | final DateTime createdAt; 20 | final GeoLocation? geoLocation; 21 | final String deviceName; 22 | final bool isThisDevice; 23 | 24 | const ActiveSession({ 25 | required this.id, 26 | required this.token, 27 | required this.lastTimeOfUse, 28 | required this.ipAddress, 29 | required this.createdAt, 30 | this.geoLocation, 31 | required this.deviceName, 32 | this.isThisDevice = false 33 | }); 34 | 35 | ActiveSession copyWith({ 36 | bool? isThisDevice 37 | }){ 38 | return ActiveSession( 39 | id: id, 40 | token: token, 41 | lastTimeOfUse: lastTimeOfUse, 42 | ipAddress: ipAddress, 43 | createdAt: createdAt, 44 | geoLocation: geoLocation, 45 | deviceName: deviceName, 46 | isThisDevice: isThisDevice ?? this.isThisDevice 47 | ); 48 | } 49 | 50 | factory ActiveSession.fromJson(Map json) => _$ActiveSessionFromJson(json); 51 | Map toJson() => _$ActiveSessionToJson(this); 52 | 53 | @override 54 | List get props => [id, token, lastTimeOfUse, ipAddress, createdAt, geoLocation, deviceName, isThisDevice]; 55 | 56 | @override 57 | bool get stringify => true; 58 | } 59 | 60 | extension ActiveSessionExtension on ActiveSession{ 61 | String deviceNameLocalization(BuildContext context){ 62 | if(deviceName == "unknown") return context.l10n.unknownDeviceName; 63 | return deviceName; 64 | } 65 | } -------------------------------------------------------------------------------- /lib/components/main/app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_route/auto_route.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/components/rounded_button.dart'; 4 | import 'package:task_manager/router/router.gr.dart'; 5 | import 'package:task_manager/theme/theme.dart'; 6 | 7 | import '../../constants.dart'; 8 | 9 | class MyAppBar extends StatelessWidget { 10 | 11 | final String header; 12 | final String description; 13 | 14 | const MyAppBar({ 15 | Key? key, 16 | required this.header, 17 | required this.description 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final customTheme = Theme.of(context).customTheme; 23 | 24 | return Column( 25 | mainAxisSize: MainAxisSize.min, 26 | children: [ 27 | Padding( 28 | padding: const EdgeInsets.all(cPadding), 29 | child: Row( 30 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 31 | children: [ 32 | 33 | // Header 34 | Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Text( 38 | header, 39 | style: customTheme.headerTextStyle, 40 | maxLines: 1 41 | ), 42 | 43 | Text( 44 | description, 45 | style: customTheme.textStyle, 46 | maxLines: 1 47 | ), 48 | ], 49 | ), 50 | 51 | // Profile 52 | RoundedButton( 53 | expandWidth: false, 54 | width: cButtonSize, 55 | color: customTheme.contentBackgroundColor, 56 | child: Image.asset( 57 | "assets/icons/profile.png" 58 | ), 59 | onPressed: () => AutoRouter.of(context).navigate(const ProfileRoute()), 60 | ), 61 | ], 62 | ), 63 | ) 64 | ], 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /lib/components/lists/rounded_dismissible.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class RoundedDismissible extends StatelessWidget{ 6 | 7 | final String text; 8 | final IconData icon; 9 | final Color color; 10 | final Widget child; 11 | final DismissDirectionCallback onDismissed; 12 | 13 | const RoundedDismissible({ 14 | Key? key, 15 | required this.text, 16 | required this.icon, 17 | required this.color, 18 | required this.child, 19 | required this.onDismissed 20 | }) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final customTheme = Theme.of(context).customTheme; 25 | 26 | return ClipRRect( 27 | borderRadius: const BorderRadius.all(Radius.circular(cBorderRadius)), 28 | child: Dismissible( 29 | key: UniqueKey(), 30 | direction: DismissDirection.endToStart, 31 | background: Container( 32 | padding: const EdgeInsets.symmetric(horizontal: cPadding), 33 | color: color, 34 | child: Row( 35 | mainAxisAlignment: MainAxisAlignment.end, 36 | children: [ 37 | Text( 38 | text, 39 | style: customTheme.textStyle.copyWith(color: Colors.white), 40 | maxLines: 1, 41 | ), 42 | const SizedBox(width: 12.0), 43 | Icon( 44 | icon, 45 | color: Colors.white, 46 | ) 47 | ], 48 | ) 49 | ), 50 | child: Container( 51 | decoration: BoxDecoration( 52 | gradient: LinearGradient( 53 | begin: Alignment.center, 54 | end: Alignment.centerRight, 55 | colors: [ 56 | customTheme.backgroundColor, 57 | color 58 | ] 59 | ) 60 | ), 61 | child: child, 62 | ), 63 | onDismissed: onDismissed, 64 | ), 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /lib/cubits/profile_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/validators/validators.dart'; 5 | 6 | enum FieldStatus { editable, saveable, saving } 7 | 8 | class ProfileState { 9 | final FieldStatus nameStatus; 10 | final String? nameError; 11 | 12 | const ProfileState({ 13 | this.nameStatus = FieldStatus.editable, 14 | this.nameError 15 | }); 16 | 17 | ProfileState copyWith({ 18 | FieldStatus? nameStatus, 19 | String? nameError 20 | }){ 21 | return ProfileState( 22 | nameStatus: nameStatus ?? this.nameStatus, 23 | nameError: nameError ?? this.nameError 24 | ); 25 | } 26 | } 27 | 28 | class ProfileCubit extends Cubit { 29 | 30 | final AuthBloc authBloc; 31 | 32 | ProfileCubit({ 33 | required this.authBloc 34 | }) : super(const ProfileState()); 35 | 36 | void namePressed({ 37 | required BuildContext context, 38 | required String name 39 | }) async{ 40 | 41 | if(state.nameStatus == FieldStatus.saveable){ 42 | final nameError = Validators.validateName(context, name); 43 | if(nameError == null){ 44 | emit(state.copyWith( 45 | nameStatus: FieldStatus.saving, 46 | nameError: null 47 | )); 48 | 49 | await Future.delayed(const Duration(milliseconds: 800)); 50 | 51 | if(name != authBloc.state.user?.name){ 52 | final updatedUser = await authBloc.userRepository.updateUser(name: name); 53 | if(updatedUser != null) authBloc.add(AuthUserChanged(updatedUser)); 54 | } 55 | 56 | emit(state.copyWith(nameStatus: FieldStatus.editable)); 57 | } 58 | else{ 59 | emit(state.copyWith( 60 | nameStatus: FieldStatus.saveable, 61 | nameError: nameError 62 | )); 63 | } 64 | } 65 | else if(state.nameStatus == FieldStatus.editable){ 66 | emit(state.copyWith( 67 | nameStatus: FieldStatus.saveable 68 | )); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /lib/components/forms/resend_code_timer.dart: -------------------------------------------------------------------------------- 1 | import 'package:custom_timer/custom_timer.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/constants.dart'; 4 | import 'package:task_manager/l10n/l10n.dart'; 5 | import 'package:task_manager/theme/theme.dart'; 6 | 7 | class ResendCodeTimer extends StatelessWidget{ 8 | 9 | final CustomTimerController controller; 10 | final void Function() onResend; 11 | 12 | const ResendCodeTimer({ 13 | Key? key, 14 | required this.controller, 15 | required this.onResend 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final customTheme = Theme.of(context).customTheme; 21 | 22 | return CustomTimer( 23 | controller: controller, 24 | begin: const Duration(minutes: 2), 25 | end: const Duration(), 26 | builder: (time) { 27 | return Text( 28 | context.l10n.emailVerification_resendCodeIn("${time.minutes}:${time.seconds}\n"), 29 | style: customTheme.smallLightTextStyle, 30 | maxLines: 2, 31 | textAlign: TextAlign.center, 32 | ); 33 | }, 34 | stateBuilder: (time, state) { 35 | return GestureDetector( 36 | child: RichText( 37 | textScaleFactor: MediaQuery.of(context).textScaleFactor, 38 | text: TextSpan( 39 | style: customTheme.smallLightTextStyle, 40 | children: [ 41 | TextSpan(text: context.l10n.emailVerification_didntReceiveCode + " "), 42 | TextSpan( 43 | text: context.l10n.emailVerification_resend + "\n", 44 | style: customTheme.smallTextButtonStyle 45 | ), 46 | ], 47 | ), 48 | maxLines: 2, 49 | textAlign: TextAlign.center, 50 | ), 51 | onTap: onResend, 52 | ); 53 | }, 54 | animationBuilder: (child) { 55 | return AnimatedSwitcher( 56 | duration: cTransitionDuration, 57 | child: child, 58 | ); 59 | }, 60 | ); 61 | } 62 | } -------------------------------------------------------------------------------- /lib/components/shimmer/shimmer_text.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:task_manager/theme/theme.dart'; 5 | 6 | class ShimmerText extends StatelessWidget{ 7 | 8 | final bool isShimmer; 9 | final double shimmerTextHeight; 10 | final int shimmerMinTextLenght; 11 | final int shimmerMaxTextLenght; 12 | final double shimmerProbability; 13 | final String? text; 14 | final TextStyle? style; 15 | final int? maxLines; 16 | final Alignment alignment; 17 | 18 | const ShimmerText({ 19 | Key? key, 20 | required this.isShimmer, 21 | this.shimmerTextHeight = 0.9, 22 | required this.shimmerMinTextLenght, 23 | required this.shimmerMaxTextLenght, 24 | this.shimmerProbability = 1.0, 25 | required this.text, 26 | this.style, 27 | this.maxLines, 28 | this.alignment = Alignment.centerLeft 29 | }) : super(key: key); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final customTheme = Theme.of(context).customTheme; 34 | 35 | if(!isShimmer) { 36 | return Text( 37 | text ?? "", 38 | style: style, 39 | maxLines: maxLines 40 | ); 41 | } 42 | 43 | bool shimmerText = Random().nextDouble() <= shimmerProbability; 44 | 45 | return shimmerText ? Stack( 46 | alignment: alignment, 47 | children: [ 48 | Text( 49 | "", 50 | style: style, 51 | maxLines: 1 52 | ), 53 | 54 | Container( 55 | decoration: BoxDecoration( 56 | borderRadius: const BorderRadius.all(Radius.circular(6.0)), 57 | color: customTheme.shimmerColor 58 | ), 59 | child: Opacity( 60 | opacity: 0, 61 | child: Text( 62 | (List.generate(shimmerMinTextLenght + Random().nextInt(shimmerMaxTextLenght - shimmerMinTextLenght), (_) => " ").join()), 63 | style: style != null ? style!.copyWith(height: shimmerTextHeight) : null, 64 | maxLines: 1 65 | ), 66 | ), 67 | ) 68 | ], 69 | ) : Container(); 70 | } 71 | } -------------------------------------------------------------------------------- /lib/bottom_sheets/time_picker_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/bottom_sheets/modal_bottom_sheet.dart'; 4 | import 'package:task_manager/components/rounded_button.dart'; 5 | import 'package:task_manager/l10n/l10n.dart'; 6 | import 'package:task_manager/theme/theme.dart'; 7 | 8 | import '../constants.dart'; 9 | 10 | class TimePickerBottomSheet{ 11 | 12 | final BuildContext context; 13 | final Duration initialTime; 14 | final Function(Duration) onTimeChanged; 15 | 16 | TimePickerBottomSheet( 17 | this.context, 18 | { 19 | required this.initialTime, 20 | required this.onTimeChanged 21 | } 22 | ); 23 | 24 | void show(){ 25 | ModalBottomSheet( 26 | title: context.l10n.bottomSheet_selectTime, 27 | context: context, 28 | content: _TimePickerBottomSheet(this) 29 | ).show(); 30 | } 31 | } 32 | 33 | class _TimePickerBottomSheet extends StatelessWidget{ 34 | 35 | final TimePickerBottomSheet data; 36 | const _TimePickerBottomSheet(this.data); 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final customTheme = Theme.of(context).customTheme; 41 | 42 | return CupertinoTheme( 43 | data: CupertinoThemeData( 44 | brightness: customTheme.isDark ? Brightness.dark : Brightness.light, 45 | ), 46 | child: Padding( 47 | padding: const EdgeInsets.symmetric(horizontal: cPadding), 48 | child: Column( 49 | children: [ 50 | Padding( 51 | padding: const EdgeInsets.symmetric(vertical: cPadding), 52 | child: CupertinoTimerPicker( 53 | mode: CupertinoTimerPickerMode.hm, 54 | initialTimerDuration: data.initialTime, 55 | onTimerDurationChanged: data.onTimeChanged 56 | ), 57 | ), 58 | RoundedTextButton( 59 | text: context.l10n.select_button, 60 | onPressed: () => Navigator.of(context).pop() 61 | ) 62 | ], 63 | ), 64 | ) 65 | ); 66 | } 67 | } -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply plugin: 'com.google.gms.google-services' 27 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 28 | 29 | android { 30 | compileSdkVersion 31 31 | 32 | sourceSets { 33 | main.java.srcDirs += 'src/main/kotlin' 34 | } 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId "com.example.task_manager" 39 | minSdkVersion 21 40 | targetSdkVersion 30 41 | versionCode flutterVersionCode.toInteger() 42 | versionName flutterVersionName 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source '../..' 56 | } 57 | 58 | dependencies { 59 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 60 | implementation platform('com.google.firebase:firebase-bom:29.0.4') 61 | implementation 'com.google.firebase:firebase-messaging' 62 | } 63 | -------------------------------------------------------------------------------- /lib/blocs/settings_cubit/settings_cubit.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings_cubit.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | SettingsState _$SettingsStateFromJson(Map json) => 10 | SettingsState( 11 | themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? 12 | ThemeMode.system, 13 | locale: const LocaleSerializer().fromJson(json['locale'] as String?), 14 | beforeScheduleNotification: 15 | json['beforeScheduleNotification'] as bool? ?? true, 16 | taskScheduleNotification: 17 | json['taskScheduleNotification'] as bool? ?? true, 18 | uncompletedTaskNotification: 19 | json['uncompletedTaskNotification'] as bool? ?? true, 20 | newUpdatesAvailableNotification: 21 | json['newUpdatesAvailableNotification'] as bool? ?? true, 22 | announcementsAndOffersNotification: 23 | json['announcementsAndOffersNotification'] as bool? ?? true, 24 | loginOnNewDeviceNotification: 25 | json['loginOnNewDeviceNotification'] as bool? ?? true, 26 | ); 27 | 28 | Map _$SettingsStateToJson(SettingsState instance) => 29 | { 30 | 'themeMode': _$ThemeModeEnumMap[instance.themeMode], 31 | 'locale': const LocaleSerializer().toJson(instance.locale), 32 | 'beforeScheduleNotification': instance.beforeScheduleNotification, 33 | 'taskScheduleNotification': instance.taskScheduleNotification, 34 | 'uncompletedTaskNotification': instance.uncompletedTaskNotification, 35 | 'newUpdatesAvailableNotification': 36 | instance.newUpdatesAvailableNotification, 37 | 'announcementsAndOffersNotification': 38 | instance.announcementsAndOffersNotification, 39 | 'loginOnNewDeviceNotification': instance.loginOnNewDeviceNotification, 40 | }; 41 | 42 | const _$ThemeModeEnumMap = { 43 | ThemeMode.system: 'system', 44 | ThemeMode.light: 'light', 45 | ThemeMode.dark: 'dark', 46 | }; 47 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: task_manager 2 | description: A new Flutter project. 3 | publish_to: 'none' 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=2.16.1 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | flutter_localizations: 14 | sdk: flutter 15 | cupertino_icons: ^1.0.4 16 | flutter_localized_locales: ^2.0.3 17 | intl: ^0.17.0 18 | 19 | bloc: ^8.0.3 20 | flutter_bloc: ^8.0.1 21 | drift: ^1.4.0 22 | sqlite3_flutter_libs: ^0.5.4 23 | path_provider: ^2.0.9 24 | bloc_concurrency: ^0.2.0 25 | equatable: ^2.0.3 26 | freezed_annotation: ^1.1.0 27 | rxdart: ^0.27.3 28 | uuid: ^3.0.6 29 | dio: ^4.0.4 30 | connectivity_plus: ^2.2.1 31 | device_info_plus: ^3.2.2 32 | jwt_decoder: ^2.0.1 33 | tuple: ^2.0.0 34 | auto_route: ^3.2.4 35 | json_annotation: ^4.4.0 36 | flutter_secure_storage: ^5.0.2 37 | get_it: ^7.2.0 38 | 39 | firebase_core: ^1.13.1 40 | firebase_messaging: ^11.2.8 41 | awesome_notifications: ^0.6.21 42 | 43 | flutter_svg: ^1.0.3 44 | flutter_map: ^0.14.0 45 | cached_network_image: ^3.2.0 46 | declarative_animated_list: ^0.1.1 47 | percent_indicator: ^4.0.0 48 | expandable_page_view: ^1.0.10 49 | tab_indicator_styler: ^2.0.0 50 | fl_chart: ^0.45.0 51 | boxy: ^2.0.5 52 | custom_timer: ^0.1.1 53 | 54 | dev_dependencies: 55 | flutter_test: 56 | sdk: flutter 57 | flutter_lints: ^1.0.4 58 | build_runner: ^2.1.7 59 | auto_route_generator: ^3.2.3 60 | freezed: ^1.1.1 61 | json_serializable: ^6.1.4 62 | drift_dev: ^1.4.0 63 | flutter_native_splash: ^2.0.5 64 | 65 | flutter_native_splash: 66 | color: "#F0F2F5" 67 | color_dark: "#1F1D2C" 68 | 69 | flutter: 70 | generate: true 71 | uses-material-design: true 72 | 73 | assets: 74 | - assets/icons/ 75 | - assets/svg/ 76 | 77 | fonts: 78 | - family: Poppins 79 | fonts: 80 | - asset: assets/fonts/Poppins-SemiBold.ttf 81 | weight: 600 82 | - asset: assets/fonts/Poppins-Medium.ttf 83 | weight: 500 84 | - asset: assets/fonts/Poppins-Regular.ttf 85 | weight: 400 86 | - asset: assets/fonts/Poppins-Light.ttf 87 | weight: 300 -------------------------------------------------------------------------------- /lib/components/responsive/fill_remaining_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:task_manager/components/responsive/widget_size.dart'; 4 | import 'package:task_manager/cubits/available_space_cubit.dart'; 5 | 6 | import '../../constants.dart'; 7 | 8 | class FillRemainingList extends StatefulWidget{ 9 | 10 | final AvailableSpaceCubit availableSpaceCubit; 11 | final Alignment alignment; 12 | final Widget child; 13 | final double subtractHeight; 14 | final bool subtractPadding; 15 | 16 | const FillRemainingList({ 17 | Key? key, 18 | required this.availableSpaceCubit, 19 | this.alignment = Alignment.center, 20 | required this.child, 21 | this.subtractHeight = 0.0, 22 | this.subtractPadding = true 23 | }) : super(key: key); 24 | 25 | @override 26 | _FillRemainingListState createState() => _FillRemainingListState(); 27 | } 28 | 29 | class _FillRemainingListState extends State{ 30 | double childHeight = 0.0; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | 35 | return BlocBuilder( 36 | bloc: widget.availableSpaceCubit, 37 | builder: (_, state) { 38 | double availableHeight = state - widget.subtractHeight; 39 | if(widget.subtractPadding) availableHeight -= cPadding * 2; 40 | 41 | return SizedBox( 42 | height: availableHeight > childHeight ? availableHeight : childHeight, 43 | child: Align( 44 | alignment: widget.alignment, 45 | child: SingleChildScrollView( 46 | physics: const NeverScrollableScrollPhysics(), 47 | child: Column( 48 | mainAxisAlignment: MainAxisAlignment.center, 49 | children: [ 50 | WidgetSize( 51 | onChange: (Size size) { 52 | setState(() => childHeight = size.height); 53 | }, 54 | child: widget.child, 55 | ) 56 | ], 57 | ), 58 | ), 59 | ), 60 | ); 61 | } 62 | ); 63 | } 64 | } -------------------------------------------------------------------------------- /lib/blocs/notifications_cubit/notifications_state.dart: -------------------------------------------------------------------------------- 1 | part of 'notifications_cubit.dart'; 2 | 3 | @JsonSerializable() 4 | class NotificationsState{ 5 | 6 | final List notifications; 7 | @JsonKey(ignore: true) 8 | final DateTime? updatedAt; 9 | 10 | NotificationsState({ 11 | required this.notifications, 12 | this.updatedAt 13 | }); 14 | 15 | static NotificationsState get initial => NotificationsState( 16 | notifications: [], 17 | updatedAt: DateTime.now() 18 | ); 19 | 20 | NotificationsState copyWith({ 21 | List? notifications 22 | }){ 23 | return NotificationsState( 24 | notifications: notifications ?? this.notifications, 25 | updatedAt: DateTime.now() 26 | ); 27 | } 28 | 29 | factory NotificationsState.fromJson(Map json) => _$NotificationsStateFromJson(json); 30 | Map toJson() => _$NotificationsStateToJson(this); 31 | } 32 | 33 | extension NotificationDataListExtension on List { 34 | List get groupByDay { 35 | List items = []; 36 | 37 | final now = DateTime.now(); 38 | final notifications = where((notification) { 39 | final scheduledAt = notification.scheduledAt; 40 | return scheduledAt == null || scheduledAt.isBefore(now); 41 | }).toList(); 42 | 43 | notifications.sort((a, b) { 44 | final dateA = a.scheduledAt ?? a.createdAt; 45 | final dateB = b.scheduledAt ?? b.createdAt; 46 | return dateB.compareTo(dateA); 47 | }); 48 | 49 | if(notifications.isNotEmpty){ 50 | DateTime lastDate = notifications.first.createdAt.ignoreTime; 51 | items.add(DynamicObject(object: lastDate)); 52 | 53 | for (NotificationData notification in notifications){ 54 | if(notification.createdAt.differenceInDays(lastDate) == 0) { 55 | items.add(DynamicObject(object: notification)); 56 | } 57 | else{ 58 | lastDate = notification.createdAt.ignoreTime; 59 | items.add(DynamicObject(object: lastDate)); 60 | items.add(DynamicObject(object: notification)); 61 | } 62 | } 63 | } 64 | return items; 65 | } 66 | } -------------------------------------------------------------------------------- /lib/blocs/settings_cubit/settings_state.dart: -------------------------------------------------------------------------------- 1 | part of 'settings_cubit.dart'; 2 | 3 | @JsonSerializable() 4 | class SettingsState{ 5 | 6 | final ThemeMode themeMode; 7 | @LocaleSerializer() 8 | final Locale? locale; 9 | 10 | final bool beforeScheduleNotification; 11 | final bool taskScheduleNotification; 12 | final bool uncompletedTaskNotification; 13 | final bool newUpdatesAvailableNotification; 14 | final bool announcementsAndOffersNotification; 15 | final bool loginOnNewDeviceNotification; 16 | 17 | SettingsState({ 18 | this.themeMode = ThemeMode.system, 19 | this.locale, 20 | this.beforeScheduleNotification = true, 21 | this.taskScheduleNotification = true, 22 | this.uncompletedTaskNotification = true, 23 | this.newUpdatesAvailableNotification = true, 24 | this.announcementsAndOffersNotification = true, 25 | this.loginOnNewDeviceNotification = true 26 | }); 27 | 28 | SettingsState copyWith({ 29 | ThemeMode? themeMode, 30 | Locale? locale, 31 | bool? beforeScheduleNotification, 32 | bool? taskScheduleNotification, 33 | bool? uncompletedTaskNotification, 34 | bool? newUpdatesAvailableNotification, 35 | bool? announcementsAndOffersNotification, 36 | bool? loginOnNewDeviceNotification, 37 | }){ 38 | return SettingsState( 39 | themeMode: themeMode ?? this.themeMode, 40 | locale: locale ?? this.locale, 41 | beforeScheduleNotification: beforeScheduleNotification ?? this.beforeScheduleNotification, 42 | taskScheduleNotification: taskScheduleNotification ?? this.taskScheduleNotification, 43 | uncompletedTaskNotification: uncompletedTaskNotification ?? this.uncompletedTaskNotification, 44 | newUpdatesAvailableNotification: newUpdatesAvailableNotification ?? this.newUpdatesAvailableNotification, 45 | announcementsAndOffersNotification: announcementsAndOffersNotification ?? this.announcementsAndOffersNotification, 46 | loginOnNewDeviceNotification: loginOnNewDeviceNotification ?? this.loginOnNewDeviceNotification 47 | ); 48 | } 49 | 50 | factory SettingsState.fromJson(Map json) => _$SettingsStateFromJson(json); 51 | Map toJson() => _$SettingsStateToJson(this); 52 | } -------------------------------------------------------------------------------- /lib/models/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:task_manager/helpers/date_time_helper.dart'; 5 | import 'package:task_manager/models/serializers/color_serializer.dart'; 6 | import 'package:task_manager/models/serializers/datetime_serializer.dart'; 7 | import 'package:uuid/uuid.dart'; 8 | 9 | part 'category.g.dart'; 10 | 11 | @JsonSerializable() 12 | class Category extends Equatable{ 13 | final String? id; 14 | final String name; 15 | @ColorSerializer() 16 | final Color color; 17 | @DateTimeSerializer() 18 | final DateTime createdAt; 19 | @DateTimeSerializer() 20 | final DateTime updatedAt; 21 | @NullableDateTimeSerializer() 22 | final DateTime? deletedAt; 23 | 24 | const Category({ 25 | required this.id, 26 | required this.name, 27 | required this.color, 28 | required this.createdAt, 29 | required this.updatedAt, 30 | this.deletedAt 31 | }); 32 | 33 | bool get isGeneral => id == null; 34 | 35 | static Category create({ 36 | bool isGeneral = false, 37 | required String name, 38 | required Color color 39 | }){ 40 | return Category( 41 | id: isGeneral ? null : const Uuid().v4(), 42 | name: name, 43 | color: color, 44 | createdAt: DateTime.now().copyWith(microsecond: 0), 45 | updatedAt: DateTime.now().copyWith(microsecond: 0) 46 | ); 47 | } 48 | 49 | Category copyWith({ 50 | String? id, 51 | String? name, 52 | Color? color, 53 | DateTime? deletedAt 54 | }){ 55 | return Category( 56 | id: id ?? this.id, 57 | name: name ?? this.name, 58 | color: color ?? this.color, 59 | createdAt: createdAt, 60 | updatedAt: DateTime.now().copyWith(microsecond: 0), 61 | deletedAt: deletedAt ?? this.deletedAt 62 | ); 63 | } 64 | 65 | factory Category.fromJson(Map json) => _$CategoryFromJson(json); 66 | Map toJson() => _$CategoryToJson(this); 67 | 68 | @override 69 | List get props => [id, name, color, createdAt, updatedAt, deletedAt]; 70 | 71 | @override 72 | bool get stringify => true; 73 | } -------------------------------------------------------------------------------- /lib/components/lists/declarative_animated_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:declarative_animated_list/declarative_animated_list.dart'; 2 | // ignore: implementation_imports 3 | import 'package:declarative_animated_list/src/algorithm/request.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:task_manager/constants.dart'; 6 | 7 | class DeclarativeAnimatedList extends StatelessWidget{ 8 | 9 | // Make the removeBuilder parameter optional. 10 | 11 | final bool shrinkWrap; 12 | final ScrollPhysics physics; 13 | final Duration insertDuration; 14 | final Duration removeDuration; 15 | final List items; 16 | final Widget Function(BuildContext context, T item, int index, Animation animation) itemBuilder; 17 | final Widget? Function(BuildContext context, T item, int index, Animation animation)? removeBuilder; 18 | final EqualityCheck? equalityCheck; 19 | final Axis scrollDirection; 20 | final bool reverse; 21 | 22 | const DeclarativeAnimatedList({ 23 | Key? key, 24 | this.shrinkWrap = true, 25 | this.physics = const NeverScrollableScrollPhysics(), 26 | this.insertDuration = cAnimatedListDuration, 27 | this.removeDuration = cAnimatedListDuration, 28 | required this.items, 29 | required this.itemBuilder, 30 | this.removeBuilder, 31 | this.equalityCheck, 32 | this.scrollDirection = Axis.vertical, 33 | this.reverse = false 34 | }) : super(key: key); 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return DeclarativeList( 39 | shrinkWrap: shrinkWrap, 40 | physics: physics, 41 | insertDuration: insertDuration, 42 | removeDuration: removeDuration, 43 | items: items, 44 | itemBuilder: itemBuilder, 45 | removeBuilder: (buildContext, T type, index, animation) { 46 | if(removeBuilder != null){ 47 | final removeItem = removeBuilder!(buildContext, type, index, animation); 48 | if(removeItem != null) return removeItem; 49 | } 50 | return itemBuilder(buildContext, type, index, animation); 51 | }, 52 | equalityCheck: equalityCheck, 53 | scrollDirection: scrollDirection, 54 | reverse: reverse, 55 | ); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/bottom_sheets/modal_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class ModalBottomSheet{ 6 | 7 | final String title; 8 | final BuildContext context; 9 | final Widget content; 10 | 11 | ModalBottomSheet({ 12 | required this.title, 13 | required this.context, 14 | required this.content 15 | }); 16 | 17 | void show(){ 18 | final customTheme = Theme.of(context).customTheme; 19 | 20 | showModalBottomSheet( 21 | isScrollControlled: true, 22 | barrierColor: cBarrierColor, 23 | backgroundColor: customTheme.backgroundColor, 24 | shape: const RoundedRectangleBorder( 25 | borderRadius: BorderRadius.only( 26 | topLeft: Radius.circular(cBottomSheetBorderRadius), 27 | topRight: Radius.circular(cBottomSheetBorderRadius) 28 | ), 29 | ), 30 | context: context, 31 | builder: (context){ 32 | return Container( 33 | padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), 34 | constraints: BoxConstraints( 35 | maxHeight: MediaQuery.of(context).size.height * 0.95 36 | ), 37 | child: SingleChildScrollView( 38 | physics: const BouncingScrollPhysics(), 39 | padding: const EdgeInsets.fromLTRB(0, 16.0, 0, cPadding), 40 | child: Column( 41 | children: [ 42 | Container( 43 | height: 4.0, 44 | width: 48.0, 45 | decoration: BoxDecoration( 46 | color: customTheme.extraLightColor, 47 | borderRadius: const BorderRadius.all(Radius.circular(8.0)) 48 | ), 49 | ), 50 | 51 | const SizedBox(height: 16.0), 52 | 53 | Text( 54 | title, 55 | style: customTheme.titleTextStyle, 56 | maxLines: 1, 57 | ), 58 | 59 | content, 60 | const SizedBox(height: 8.0), 61 | ], 62 | ), 63 | ) 64 | ); 65 | } 66 | ); 67 | } 68 | } -------------------------------------------------------------------------------- /lib/cubits/change_email_verification_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/repositories/auth_repository.dart'; 5 | import 'package:task_manager/validators/validators.dart'; 6 | 7 | class ChangeEmailVerificationState { 8 | final bool isLoading; 9 | final String? codeError; 10 | final bool changed; 11 | 12 | const ChangeEmailVerificationState({ 13 | this.isLoading = false, 14 | this.codeError, 15 | this.changed = false 16 | }); 17 | } 18 | 19 | class ChangeEmailVerificationCubit extends Cubit { 20 | 21 | final AuthRepository authRepository; 22 | final AuthBloc authBloc; 23 | 24 | ChangeEmailVerificationCubit({ 25 | required this.authRepository, 26 | required this.authBloc 27 | }) : super(const ChangeEmailVerificationState()); 28 | 29 | void submitted({ 30 | required BuildContext context, 31 | required String code 32 | }) async{ 33 | 34 | final codeError = Validators.validateEmailVerificationCode(context, code); 35 | 36 | if(codeError == null){ 37 | emit(const ChangeEmailVerificationState(isLoading: true)); 38 | 39 | final response = await authRepository.verifyChangeEmailCode( 40 | code: code, 41 | ignoreKeys: ["code"] 42 | ); 43 | 44 | if(response != null) { 45 | response.when( 46 | left: (responseMessage){ 47 | emit(ChangeEmailVerificationState( 48 | isLoading: false, 49 | codeError: Validators.validateEmailVerificationCodeResponse(context, responseMessage) 50 | ?? responseMessage.get("code"), 51 | )); 52 | }, 53 | 54 | right: (user){ 55 | authBloc.add(AuthUserChanged(user)); 56 | emit(const ChangeEmailVerificationState(changed: true)); 57 | }, 58 | ); 59 | } else { 60 | emit(const ChangeEmailVerificationState(isLoading: false)); 61 | } 62 | } 63 | else{ 64 | emit(ChangeEmailVerificationState( 65 | isLoading: false, 66 | codeError: codeError 67 | )); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /lib/blocs/drifted_bloc/drifted_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:task_manager/blocs/drifted_bloc/drifted_database.dart'; 4 | import 'package:task_manager/blocs/drifted_bloc/drifted_isolate.dart'; 5 | import 'package:collection/collection.dart'; 6 | 7 | abstract class Storage { 8 | DriftedState? read(String key); 9 | Future write(String key, dynamic state, DateTime updatedAt); 10 | Future delete(String key); 11 | Stream? watch(String key); 12 | Future clear(); 13 | } 14 | 15 | 16 | 17 | class DriftedStorage implements Storage { 18 | static DriftedStorage? _instance; 19 | static List? _states; 20 | static StreamController? _statesController; 21 | 22 | final DriftedDatabase _database; 23 | DriftedStorage(this._database); 24 | 25 | static Future build() async{ 26 | if(_instance != null) return _instance!; 27 | 28 | final database = await DriftedIsolate.createOrGetDatabase(); 29 | _states = await database.getStates(); 30 | 31 | _statesController = StreamController.broadcast(); 32 | database.watch?.listen((driftedStates) { 33 | _states = driftedStates; 34 | _statesController?.add(null); 35 | }); 36 | 37 | return _instance = DriftedStorage(database); 38 | } 39 | 40 | @override 41 | DriftedState? read(String key) { 42 | final df = _states?.firstWhereOrNull((s) => s.key == key); 43 | return df; 44 | } 45 | 46 | @override 47 | Future write(String key, state, DateTime updatedAt) => _database.insertOrReplaceState( 48 | DriftedState( 49 | key: key, 50 | state: state, 51 | updatedAt: updatedAt 52 | ) 53 | ); 54 | 55 | @override 56 | Future delete(String key) => _database.deleteState(key); 57 | 58 | @override 59 | Stream? watch(String key) { 60 | return _statesController?.stream.transform(singleStateOrNull(key)); 61 | } 62 | 63 | @override 64 | Future clear() => _database.clear(); 65 | 66 | StreamTransformer singleStateOrNull(String key) { 67 | return StreamTransformer.fromHandlers(handleData: (data, sink) { 68 | sink.add(_states?.firstWhereOrNull((s) => s.key == key)); 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /lib/cubits/forgot_password_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/repositories/auth_repository.dart'; 4 | import 'package:task_manager/validators/validators.dart'; 5 | 6 | class ForgotPasswordState { 7 | final bool isLoading; 8 | final String? emailError; 9 | final bool emailSent; 10 | 11 | const ForgotPasswordState({ 12 | this.isLoading = false, 13 | this.emailError, 14 | this.emailSent = false 15 | }); 16 | } 17 | 18 | class ForgotPasswordCubit extends Cubit { 19 | 20 | final AuthRepository authRepository; 21 | ForgotPasswordCubit({ 22 | required this.authRepository 23 | }) : super(const ForgotPasswordState()); 24 | 25 | void submitted({ 26 | required BuildContext context, 27 | required String email 28 | }) async{ 29 | 30 | final emailError = Validators.validateEmail(context, email); 31 | 32 | if(emailError == null){ 33 | emit(const ForgotPasswordState(isLoading: true)); 34 | final response = await authRepository.sendPasswordResetCode( 35 | email: email, 36 | ignoreKeys: ["user", "email"], 37 | ignoreFunction: (m) => DateTime.tryParse(m.toUpperCase()) != null 38 | ); 39 | 40 | if(response != null) { 41 | response.when( 42 | left: (responseMessage){ 43 | final dateTime = DateTime.tryParse(responseMessage.first.toUpperCase()); 44 | 45 | if(dateTime != null) { 46 | emit(const ForgotPasswordState(emailSent: true)); 47 | } else{ 48 | emit(ForgotPasswordState( 49 | isLoading: false, 50 | emailError: Validators.validateEmailResponse(context, responseMessage) 51 | ?? (responseMessage.get("user") ?? responseMessage.get("email")), 52 | )); 53 | } 54 | }, 55 | 56 | right: (sent) => emit(const ForgotPasswordState(emailSent: true)) 57 | ); 58 | } else { 59 | emit(const ForgotPasswordState(isLoading: false)); 60 | } 61 | } 62 | else{ 63 | emit(ForgotPasswordState( 64 | isLoading: false, 65 | emailError: emailError, 66 | )); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /lib/cubits/forgot_password_new_password_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/models/auth_credentials.dart'; 5 | import 'package:task_manager/repositories/auth_repository.dart'; 6 | import 'package:task_manager/validators/validators.dart'; 7 | 8 | class ForgotPasswordNewPasswordState { 9 | final bool isLoading; 10 | final String? passwordError; 11 | final bool changed; 12 | 13 | const ForgotPasswordNewPasswordState({ 14 | this.isLoading = false, 15 | this.passwordError, 16 | this.changed = false 17 | }); 18 | } 19 | 20 | class ForgotPasswordNewPasswordCubit extends Cubit { 21 | 22 | final AuthRepository authRepository; 23 | final AuthBloc authBloc; 24 | 25 | ForgotPasswordNewPasswordCubit({ 26 | required this.authRepository, 27 | required this.authBloc 28 | }) : super(const ForgotPasswordNewPasswordState()); 29 | 30 | void submitted({ 31 | required BuildContext context, 32 | required String password 33 | }) async{ 34 | 35 | final passwordError = Validators.validatePassword(context, password); 36 | 37 | if(passwordError == null){ 38 | emit(const ForgotPasswordNewPasswordState(isLoading: true)); 39 | 40 | final response = await authRepository.changeForgotPassword( 41 | password: password, 42 | ignoreKeys: ["password"] 43 | ); 44 | 45 | if(response != null) { 46 | response.when( 47 | left: (responseMessage) => emit(ForgotPasswordNewPasswordState( 48 | isLoading: false, 49 | passwordError: Validators.validatePasswordResponse(context, responseMessage) 50 | ?? responseMessage.get("password") 51 | )), 52 | 53 | right: (changed){ 54 | emit(const ForgotPasswordNewPasswordState(changed: true)); 55 | authBloc.add(AuthCredentialsChanged(AuthCredentials.empty)); 56 | }, 57 | ); 58 | } else { 59 | emit(const ForgotPasswordNewPasswordState(isLoading: false)); 60 | } 61 | } 62 | else{ 63 | emit(ForgotPasswordNewPasswordState( 64 | isLoading: false, 65 | passwordError: passwordError, 66 | )); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /lib/components/lists/animated_dynamic_task_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:task_manager/blocs/task_bloc/task_bloc.dart'; 4 | import 'package:task_manager/components/lists/declarative_animated_list.dart'; 5 | import 'package:task_manager/components/lists/list_item_animation.dart'; 6 | import 'package:task_manager/components/lists/task_list_item.dart'; 7 | import 'package:task_manager/models/dynamic_object.dart'; 8 | import 'package:task_manager/models/task.dart'; 9 | import 'package:collection/collection.dart'; 10 | 11 | class AnimatedDynamicTaskList extends StatelessWidget{ 12 | final List items; 13 | final TaskListItemType taskListItemType; 14 | final Widget Function(Object) objectBuilder; 15 | final BuildContext buildContext; 16 | final Function(Task) onUndoDismissed; 17 | 18 | const AnimatedDynamicTaskList({ 19 | Key? key, 20 | required this.items, 21 | required this.taskListItemType, 22 | required this.objectBuilder, 23 | required this.buildContext, 24 | required this.onUndoDismissed 25 | }) : super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext context){ 29 | 30 | return DeclarativeAnimatedList( 31 | items: items, 32 | equalityCheck: (DynamicObject a, DynamicObject b) => a.object == b.object, 33 | itemBuilder: (BuildContext buildContext, DynamicObject dynamicObject, int index, Animation animation){ 34 | final dynamic item = dynamicObject.object; 35 | 36 | return ListItemAnimation( 37 | animation: animation, 38 | child: item is Task ? TaskListItem( 39 | task: item, 40 | type: taskListItemType, 41 | buildContext: buildContext, 42 | onUndoDismissed: onUndoDismissed 43 | ) : objectBuilder(item) 44 | ); 45 | }, 46 | removeBuilder: (BuildContext buildContext, DynamicObject dynamicObject, int index, Animation animation){ 47 | final object = dynamicObject.object; 48 | 49 | if(object is Task){ 50 | final taskState = buildContext.read().state; 51 | if(taskState.deletedTasks.firstWhereOrNull((t) => t.id == object.id) != null){ 52 | return Container(); 53 | } 54 | } 55 | return null; 56 | }, 57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /lib/cubits/login_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/repositories/auth_repository.dart'; 5 | import 'package:task_manager/validators/validators.dart'; 6 | 7 | class LoginState { 8 | final bool isLoading; 9 | final String? emailError; 10 | final String? passwordError; 11 | 12 | const LoginState({ 13 | this.isLoading = false, 14 | this.emailError, 15 | this.passwordError, 16 | }); 17 | } 18 | 19 | class LoginCubit extends Cubit { 20 | 21 | final AuthRepository authRepository; 22 | final AuthBloc authBloc; 23 | 24 | LoginCubit({ 25 | required this.authRepository, 26 | required this.authBloc 27 | }) : super(const LoginState()); 28 | 29 | void submitted({ 30 | required BuildContext context, 31 | required String email, 32 | required String password 33 | }) async{ 34 | 35 | final emailError = Validators.validateEmail(context, email); 36 | final passwordError = Validators.validatePassword(context, password); 37 | 38 | if(emailError == null && passwordError == null){ 39 | emit(const LoginState(isLoading: true)); 40 | 41 | final response = await authRepository.login( 42 | email: email, 43 | password: password, 44 | ignoreKeys: ["user", "email", "password"] 45 | ); 46 | 47 | if(response != null){ 48 | response.when( 49 | left: (responseMessage) => emit(LoginState( 50 | isLoading: false, 51 | 52 | emailError: Validators.validateEmailResponse(context, responseMessage) 53 | ?? (responseMessage.get("user") ?? responseMessage.get("email")), 54 | 55 | passwordError: Validators.validatePasswordResponse(context, responseMessage) 56 | ?? responseMessage.get("password") 57 | )), 58 | 59 | right: (authCredentials){ 60 | emit(const LoginState(isLoading: false)); 61 | authBloc.add(AuthCredentialsChanged(authCredentials)); 62 | }, 63 | ); 64 | } else { 65 | emit(const LoginState(isLoading: false)); 66 | } 67 | } 68 | else{ 69 | emit(LoginState( 70 | isLoading: false, 71 | emailError: emailError, 72 | passwordError: passwordError, 73 | )); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /lib/repositories/sync_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:task_manager/helpers/response_errors.dart'; 6 | import 'package:task_manager/models/category.dart'; 7 | import 'package:task_manager/models/either.dart'; 8 | import 'package:task_manager/models/serializers/datetime_serializer.dart'; 9 | import 'package:task_manager/models/task.dart'; 10 | import 'package:task_manager/repositories/base_repository.dart'; 11 | import 'package:tuple/tuple.dart'; 12 | 13 | class SyncRepository{ 14 | 15 | final BaseRepository base; 16 | SyncRepository({required this.base}); 17 | 18 | Future, 20 | Tuple2, List> 21 | >?> sync({ 22 | required DateTime? lastSync, 23 | required List tasks, 24 | required List categories 25 | }) async { 26 | try{ 27 | 28 | final dio = await base.dioAccessToken; 29 | final response = await dio.post( 30 | "/sync", 31 | data: { 32 | "lastSync": const NullableDateTimeSerializer().toJson(lastSync), 33 | "tasks": tasks, 34 | "categories": categories 35 | } 36 | ); 37 | 38 | return Right(Tuple2( 39 | List.from(response.data["tasks"].map((t) => Task.fromJson(t))), 40 | List.from(response.data["categories"].map((c) => Category.fromJson(c))) 41 | )); 42 | } 43 | catch (error){ 44 | if(error is DioError){ 45 | 46 | final dioError = error.response?.data["message"]; 47 | debugPrint("SyncRepository | DioError: $dioError"); 48 | } 49 | else{ 50 | debugPrint("SyncRepository | Error: $error"); 51 | } 52 | 53 | final responseMessage = await ResponseError.validate(error, ["duplicated"]); 54 | if(responseMessage == null) return null; 55 | 56 | String? duplicatedMessage = responseMessage.get("duplicated"); 57 | if(duplicatedMessage == null) return null; 58 | 59 | final duplicatedId = duplicatedMessage.split(":").last; 60 | 61 | duplicatedMessage = duplicatedMessage.toLowerCase(); 62 | if(duplicatedMessage.contains("task")) return Left(Tuple2(duplicatedId, Task)); 63 | if(duplicatedMessage.contains("category")) return Left(Tuple2(duplicatedId, Category)); 64 | } 65 | return null; 66 | } 67 | } -------------------------------------------------------------------------------- /lib/components/responsive/centered_page_view_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:task_manager/components/responsive/widget_size.dart'; 4 | import 'package:task_manager/cubits/available_space_cubit.dart'; 5 | 6 | class CenteredPageViewWidget extends StatefulWidget{ 7 | 8 | final AvailableSpaceCubit availableSpaceCubit; 9 | final PageController? controller; 10 | final ScrollPhysics? physics; 11 | final void Function(int)? onPageChanged; 12 | final List children; 13 | 14 | const CenteredPageViewWidget({ 15 | Key? key, 16 | required this.availableSpaceCubit, 17 | this.controller, 18 | this.physics, 19 | this.onPageChanged, 20 | required this.children 21 | }) : super(key: key); 22 | 23 | @override 24 | _CenteredPageViewWidgetState createState() => _CenteredPageViewWidgetState(); 25 | } 26 | 27 | class _CenteredPageViewWidgetState extends State{ 28 | double childHeight = 0.0; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | 33 | return BlocBuilder( 34 | bloc: widget.availableSpaceCubit, 35 | builder: (_, availableHeight) { 36 | 37 | return SizedBox( 38 | height: availableHeight > childHeight ? availableHeight : childHeight, 39 | child: PageView.builder( 40 | controller: widget.controller, 41 | physics: widget.physics, 42 | onPageChanged: widget.onPageChanged, 43 | itemCount: widget.children.length, 44 | itemBuilder: (context, index){ 45 | 46 | return Center( 47 | child: SingleChildScrollView( 48 | physics: const NeverScrollableScrollPhysics(), 49 | child: Column( 50 | mainAxisAlignment: MainAxisAlignment.center, 51 | children: [ 52 | WidgetSize( 53 | onChange: (Size size) { 54 | setState(() => childHeight = size.height); 55 | }, 56 | child: widget.children.elementAt(index) 57 | ) 58 | ], 59 | ), 60 | ), 61 | ); 62 | } 63 | ) 64 | ); 65 | } 66 | ); 67 | } 68 | } -------------------------------------------------------------------------------- /lib/cubits/change_password_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/repositories/auth_repository.dart'; 4 | import 'package:task_manager/validators/validators.dart'; 5 | 6 | class ChangePasswordState { 7 | final bool isLoading; 8 | final String? currentPasswordError; 9 | final String? newPasswordError; 10 | final bool changed; 11 | 12 | const ChangePasswordState({ 13 | this.isLoading = false, 14 | this.currentPasswordError, 15 | this.newPasswordError, 16 | this.changed = false 17 | }); 18 | } 19 | 20 | class ChangePasswordCubit extends Cubit { 21 | 22 | final AuthRepository authRepository; 23 | ChangePasswordCubit({ 24 | required this.authRepository 25 | }) : super(const ChangePasswordState()); 26 | 27 | void submitted({ 28 | required BuildContext context, 29 | required String currentPassword, 30 | required String newPassword 31 | }) async{ 32 | 33 | final currentPasswordError = Validators.validateCurrentPassword(context, currentPassword); 34 | final newPasswordError = Validators.validateNewPassword(context, currentPassword, newPassword); 35 | 36 | if(currentPasswordError == null && newPasswordError == null){ 37 | emit(const ChangePasswordState(isLoading: true)); 38 | 39 | final response = await authRepository.changePassword( 40 | currentPassword: currentPassword, 41 | newPassword: newPassword, 42 | ignoreKeys: ["password"] 43 | ); 44 | 45 | if(response != null) { 46 | response.when( 47 | left: (responseMessage) => emit(ChangePasswordState( 48 | isLoading: false, 49 | currentPasswordError: Validators.validatePasswordResponse(context, responseMessage) 50 | ?? responseMessage.getIgnoring("password", ignore: "newPassword"), 51 | newPasswordError: responseMessage.get("newPassword") 52 | )), 53 | 54 | right: (changed){ 55 | emit(const ChangePasswordState(changed: true)); 56 | }, 57 | ); 58 | } else { 59 | emit(const ChangePasswordState(isLoading: false)); 60 | } 61 | } 62 | else{ 63 | emit(ChangePasswordState( 64 | isLoading: false, 65 | currentPasswordError: currentPasswordError, 66 | newPasswordError: newPasswordError 67 | )); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /lib/components/rounded_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/theme/theme.dart'; 4 | 5 | class RoundedButton extends StatelessWidget{ 6 | 7 | final Widget child; 8 | final Color color; 9 | final bool expandWidth; 10 | final double? width; 11 | final double height; 12 | final double borderRadius; 13 | final EdgeInsets padding; 14 | final void Function()? onPressed; 15 | 16 | const RoundedButton({ 17 | Key? key, 18 | required this.child, 19 | this.color = cPrimaryColor, 20 | this.expandWidth = true, 21 | this.width, 22 | this.height = cButtonSize, 23 | this.borderRadius = cBorderRadius, 24 | this.padding = const EdgeInsets.all(cButtonPadding), 25 | required this.onPressed 26 | }) : super(key: key); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final customTheme = Theme.of(context).customTheme; 31 | 32 | return SizedBox( 33 | width: expandWidth ? double.infinity : width, 34 | height: height, 35 | child: ElevatedButton( 36 | onPressed: onPressed, 37 | child: child, 38 | style: ElevatedButton.styleFrom( 39 | primary: color, 40 | padding: padding, 41 | shape: RoundedRectangleBorder( 42 | borderRadius: BorderRadius.circular(borderRadius), 43 | ), 44 | elevation: customTheme.elevation, 45 | shadowColor: customTheme.shadowColor, 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | class RoundedTextButton extends StatelessWidget{ 53 | 54 | final String text; 55 | final EdgeInsets textPadding; 56 | final bool expandWidth; 57 | final void Function()? onPressed; 58 | 59 | const RoundedTextButton({ 60 | Key? key, 61 | required this.text, 62 | this.textPadding = EdgeInsets.zero, 63 | this.expandWidth = true, 64 | required this.onPressed 65 | }) : super(key: key); 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | final customTheme = Theme.of(context).customTheme; 70 | 71 | return RoundedButton( 72 | expandWidth: expandWidth, 73 | child: Padding( 74 | padding: textPadding, 75 | child: Text( 76 | text, 77 | style: customTheme.primaryColorButtonTextStyle, 78 | maxLines: 1, 79 | ), 80 | ), 81 | onPressed: onPressed 82 | ); 83 | } 84 | } -------------------------------------------------------------------------------- /lib/cubits/email_verification_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/repositories/auth_repository.dart'; 5 | import 'package:task_manager/validators/validators.dart'; 6 | 7 | class EmailVerificationState { 8 | final bool isLoading; 9 | final String? codeError; 10 | 11 | const EmailVerificationState({ 12 | this.isLoading = false, 13 | this.codeError 14 | }); 15 | } 16 | 17 | class EmailVerificationCubit extends Cubit { 18 | 19 | final AuthRepository authRepository; 20 | final AuthBloc authBloc; 21 | 22 | EmailVerificationCubit({ 23 | required this.authRepository, 24 | required this.authBloc 25 | }) : super(const EmailVerificationState()){ 26 | sendAccountVerificationCode(); 27 | } 28 | 29 | void submitted({ 30 | required BuildContext context, 31 | required String code 32 | }) async{ 33 | 34 | final codeError = Validators.validateEmailVerificationCode(context, code); 35 | 36 | if(codeError == null){ 37 | emit(const EmailVerificationState(isLoading: true)); 38 | 39 | final response = await authRepository.verifyAccountCode( 40 | code: code, 41 | ignoreKeys: ["code"] 42 | ); 43 | 44 | if(response != null) { 45 | response.when( 46 | left: (responseMessage) => emit(EmailVerificationState( 47 | isLoading: false, 48 | codeError: Validators.validateEmailVerificationCodeResponse(context, responseMessage) 49 | ?? responseMessage.get("code"), 50 | )), 51 | 52 | right: (accessToken) async { 53 | emit(const EmailVerificationState(isLoading: false)); 54 | final currentCredentials = await authBloc.secureStorageRepository.read.authCredentials; 55 | authBloc.add(AuthCredentialsChanged(currentCredentials.copyWith(accessToken: accessToken))); 56 | }, 57 | ); 58 | } else { 59 | emit(const EmailVerificationState(isLoading: false)); 60 | } 61 | } 62 | else{ 63 | emit(EmailVerificationState( 64 | isLoading: false, 65 | codeError: codeError, 66 | )); 67 | } 68 | } 69 | 70 | void sendAccountVerificationCode(){ 71 | authRepository.sendAccountVerificationCode( 72 | ignoreFunction: (m) => DateTime.tryParse(m.toUpperCase()) != null 73 | ); 74 | } 75 | } -------------------------------------------------------------------------------- /lib/components/calendar/calendar_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | import 'package:task_manager/helpers/date_time_helper.dart'; 4 | import 'package:task_manager/theme/theme.dart'; 5 | 6 | class CalendarCard extends StatelessWidget{ 7 | 8 | final DateTime dateTime; 9 | final bool isSelected; 10 | final Function()? onTap; 11 | 12 | const CalendarCard({ 13 | Key? key, 14 | required this.dateTime, 15 | required this.isSelected, 16 | this.onTap 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final customTheme = Theme.of(context).customTheme; 22 | 23 | return GestureDetector( 24 | child: AnimatedContainer( 25 | duration: cFastAnimationDuration, 26 | decoration: BoxDecoration( 27 | borderRadius: const BorderRadius.all(Radius.circular(cBorderRadius)), 28 | color: isSelected ? cPrimaryColor : Colors.transparent 29 | ), 30 | padding: const EdgeInsets.symmetric( 31 | vertical: 16.0, 32 | horizontal: 12.0 33 | ), 34 | child: Column( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Text( 38 | dateTime.format(context, "d"), 39 | style: customTheme.subtitleTextStyle.copyWith( 40 | color: isSelected ? Colors.white : customTheme.lightTextColor 41 | ), 42 | maxLines: 1, 43 | ), 44 | const SizedBox(height: 4.0), 45 | 46 | Stack( 47 | alignment: Alignment.center, 48 | children: [ 49 | // Text to avoid the width change generated by the names of the days. 50 | Opacity( 51 | opacity: 0.0, 52 | child: Text( 53 | "aaaa", 54 | style: customTheme.smallLightTextStyle, 55 | maxLines: 1, 56 | ), 57 | ), 58 | 59 | Text( 60 | dateTime.format(context, "E"), 61 | style: customTheme.smallLightTextStyle.copyWith( 62 | color: isSelected ? Colors.white : customTheme.lightTextColor 63 | ), 64 | maxLines: 1, 65 | ) 66 | ], 67 | ), 68 | ], 69 | ), 70 | ), 71 | onTap: onTap, 72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 9 | 17 | 21 | 25 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/cubits/forgot_password_email_verification_cubit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:task_manager/blocs/auth_bloc/auth_bloc.dart'; 4 | import 'package:task_manager/repositories/auth_repository.dart'; 5 | import 'package:task_manager/validators/validators.dart'; 6 | 7 | class ForgotPasswordEmailVerificationState { 8 | final bool isLoading; 9 | final String? codeError; 10 | final bool verified; 11 | 12 | const ForgotPasswordEmailVerificationState({ 13 | this.isLoading = false, 14 | this.codeError, 15 | this.verified = false 16 | }); 17 | } 18 | 19 | class ForgotPasswordEmailVerificationCubit extends Cubit { 20 | 21 | final AuthRepository authRepository; 22 | final AuthBloc authBloc; 23 | final String email; 24 | 25 | ForgotPasswordEmailVerificationCubit({ 26 | required this.authRepository, 27 | required this.authBloc, 28 | required this.email 29 | }) : super(const ForgotPasswordEmailVerificationState()); 30 | 31 | void submitted({ 32 | required BuildContext context, 33 | required String code 34 | }) async{ 35 | 36 | final codeError = Validators.validateEmailVerificationCode(context, code); 37 | 38 | if(codeError == null){ 39 | emit(const ForgotPasswordEmailVerificationState(isLoading: true)); 40 | 41 | final response = await authRepository.verifyPasswordCode( 42 | email: email, 43 | code: code, 44 | ignoreKeys: ["code"] 45 | ); 46 | 47 | if(response != null) { 48 | response.when( 49 | left: (responseMessage) => emit(ForgotPasswordEmailVerificationState( 50 | isLoading: false, 51 | codeError: Validators.validateEmailVerificationCodeResponse(context, responseMessage) 52 | ?? responseMessage.get("code"), 53 | )), 54 | 55 | right: (credentials){ 56 | emit(const ForgotPasswordEmailVerificationState(verified: true)); 57 | authBloc.add(AuthCredentialsChanged(credentials)); 58 | }, 59 | ); 60 | } else { 61 | emit(const ForgotPasswordEmailVerificationState(isLoading: false)); 62 | } 63 | } 64 | else{ 65 | emit(ForgotPasswordEmailVerificationState( 66 | isLoading: false, 67 | codeError: codeError, 68 | )); 69 | } 70 | } 71 | 72 | void sendPasswordResetCode(){ 73 | authRepository.sendPasswordResetCode( 74 | email: email 75 | ); 76 | } 77 | } -------------------------------------------------------------------------------- /lib/components/forms/form_validator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:task_manager/constants.dart'; 3 | 4 | class FormValidator extends StatefulWidget{ 5 | 6 | final Widget Function(FormFieldState) widget; 7 | final String? Function(dynamic)? validator; 8 | final String? errorText; 9 | final EdgeInsets errorTextPadding; 10 | 11 | const FormValidator({ 12 | Key? key, 13 | required this.widget, 14 | this.validator, 15 | this.errorText, 16 | this.errorTextPadding = const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0) 17 | }) : super(key: key); 18 | 19 | @override 20 | State createState() => _FormValidatorState(); 21 | } 22 | 23 | class _FormValidatorState extends State { 24 | 25 | bool errorCurrentState = false; 26 | bool animate = true; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | 31 | ThemeData themeData = Theme.of(context); 32 | 33 | return FormField( 34 | builder: (FormFieldState state){ 35 | 36 | bool hasError; 37 | if(widget.errorText != null) { 38 | hasError = widget.errorText != null; 39 | } else { 40 | hasError = state.hasError; 41 | } 42 | 43 | if(errorCurrentState && !hasError) { 44 | animate = false; 45 | } 46 | else { 47 | animate = true; 48 | } 49 | 50 | errorCurrentState = hasError; 51 | 52 | return Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | widget.widget(state), 56 | 57 | animate ? AnimatedCrossFade( 58 | duration: cTransitionDuration, 59 | crossFadeState: hasError ? CrossFadeState.showFirst : CrossFadeState.showSecond, 60 | firstChild: Padding( 61 | padding: widget.errorTextPadding, 62 | child: Row( 63 | mainAxisAlignment: MainAxisAlignment.center, 64 | children: [ 65 | Text( 66 | (widget.errorText ?? state.errorText) ?? "", 67 | style: themeData.textTheme.caption!.copyWith(color: themeData.errorColor), 68 | textAlign: TextAlign.center, 69 | maxLines: 1, 70 | ) 71 | ], 72 | ), 73 | ), 74 | secondChild: Container() 75 | ) : Container() 76 | ], 77 | ); 78 | }, 79 | validator: widget.validator, 80 | ); 81 | } 82 | } --------------------------------------------------------------------------------