diff --git a/.gitignore b/.gitignore index 7b790f8..fe28037 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ -# Created by https://www.gitignore.io/api/dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio -# Edit at https://www.gitignore.io/?templates=dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Created by https://www.toptal.com/developers/gitignore/api/dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio ### Android ### # Built application files *.apk +*.aar *.ap_ *.aab @@ -18,6 +35,8 @@ bin/ gen/ out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ # Gradle files .gradle/ @@ -46,7 +65,11 @@ captures/ .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries +# Android Studio 3 in .gitignore file. .idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. @@ -55,9 +78,10 @@ captures/ # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild +.cxx/ # Google Services (e.g. APIs or Firebase) -google-services.json +# google-services.json # Freeline freeline.py @@ -71,117 +95,22 @@ fastlane/screenshots fastlane/test_output fastlane/readme.md -### Android Patch ### -gen-external-apklibs - -### AndroidStudio ### -# Covers files to be ignored for android development using Android Studio. - -# Built application files - -# Files for the ART/Dalvik VM - -# Java class files - -# Generated files - -# Gradle files -.gradle - -# Signing files -.signing/ - -# Local configuration file (sdk path, etc) - -# Proguard folder generated by Eclipse - -# Log Files - -# Android Studio -/*/build/ -/*/local.properties -/*/out -/*/*/build -/*/*/production -*.ipr -*~ -*.swp +# Version control +vcs.xml -# Android Patch - -# External native build folder generated in Android Studio 2.2 and later - -# NDK -obj/ +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ -# IntelliJ IDEA -*.iws -/out/ - -# User-specific configurations -.idea/caches/ -.idea/libraries/ -.idea/shelf/ -.idea/.name -.idea/compiler.xml -.idea/copyright/profiles_settings.xml -.idea/encodings.xml -.idea/misc.xml -.idea/modules.xml -.idea/scopes/scope_settings.xml -.idea/vcs.xml -.idea/jsLibraryMappings.xml -.idea/datasources.xml -.idea/dataSources.ids -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml - -# OS-specific files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Legacy Eclipse project files -.classpath -.project -.cproject -.settings/ - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.war -*.ear - -# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) -hs_err_pid* - -## Plugin-specific files: - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Mongo Explorer plugin -.idea/mongoSettings.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### AndroidStudio Patch ### +### Android Patch ### +gen-external-apklibs +output.json -!/gradle/wrapper/gradle-wrapper.jar +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. ### CocoaPods ### ## CocoaPods GitIgnore Template @@ -214,9 +143,63 @@ doc/api/ *.js.map ### Flutter ### +# Flutter/Dart/Pub related +**/doc/api/ .flutter-plugins +.flutter-plugins-dependencies +.fvm/ +.pub-cache/ +.pub/ +lib/generated_plugin_registrant.dart + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/key.properties +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages ### Linux ### +*~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* @@ -232,18 +215,23 @@ doc/api/ ### macOS ### # General +.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon + # Thumbnails +._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd +.Spotlight-V100 .TemporaryItems +.Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent @@ -256,6 +244,9 @@ Temporary Items ### Windows ### # Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db ehthumbs_vista.db # Dump file @@ -301,15 +292,126 @@ DerivedData/ *.perspectivev3 !default.perspectivev3 +## Gcc Patch +/*.gcno + ### Xcode Patch ### *.xcodeproj/* !*.xcodeproj/project.pbxproj !*.xcodeproj/xcshareddata/ !*.xcworkspace/contents.xcworkspacedata -/*.gcno **/xcshareddata/WorkspaceSettings.xcsettings -# End of https://www.gitignore.io/api/dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/jarRepositories.xml + +# OS-specific files +.DS_Store? + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/dart,xcode,linux,macos,windows,android,flutter,cocoapods,androidstudio ### Custom ### @@ -328,4 +430,7 @@ packages ios/.symlinks/ # Plugins -secrets.json \ No newline at end of file +config.json + +# Flutter Plugin +.flutter-plugins-dependencies \ No newline at end of file diff --git a/README.md b/README.md index b90422e..4a2b83d 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,21 @@ A new Flutter application about "resumes". For help getting started with Flutter, view our online [documentation](https://flutter.io/). +## Directory structure + +- Date: +Contains data layer shared classes that are not framework dependent (e.g. Flutter, AngularDart) or interfaces that must be used by client application +- Domain: +Contains domain layer company rules +- Bloc: +Contains shared business logic components +- Presentation: +Contains commons classes + ## Installation ### Config -Use secret template `./secrets.json.dist` to add secret file `./secrets.json` +Use secret template `./config.json.dist` to add secret file `./config.json` ### Generate models [documentation](https://flutter.io/json/) diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..ff48c07 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,93 @@ +analyzer: + strong-mode: + implicit-casts: false + +linter: + rules: + - always_declare_return_types + - always_require_non_null_named_parameters + - annotate_overrides + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_field_initializers_in_const_classes + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_types + - cancel_subscriptions + - close_sinks + - constant_identifier_names + - control_flow_in_finally + - directives_ordering + - empty_catches + - empty_constructor_bodies + - empty_statements + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - library_names + - library_prefixes + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_duplicate_case_values + - non_constant_identifier_names + - null_closures + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_equal_for_default_values + - prefer_final_fields + - prefer_final_locals + - prefer_foreach + - prefer_generic_function_type_aliases + - prefer_interpolation_to_compose_strings + - prefer_is_not_empty + - prefer_iterable_whereType + - prefer_single_quotes + - prefer_typing_uninitialized_variables + - recursive_getters + - slash_for_doc_comments + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_statements + - unnecessary_this + - unrelated_type_equality_checks + - use_rethrow_when_possible + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..aba602b --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c0758fb..cf73c6a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - - + + + + android:name="io.flutter.embedding.android.SplashScreenDrawable" + android:resource="@drawable/launch_background" + /> + + diff --git a/android/app/src/main/java/me/lebot/axel/cv/MainActivity.java b/android/app/src/main/java/me/lebot/axel/cv/MainActivity.java deleted file mode 100644 index 01f2582..0000000 --- a/android/app/src/main/java/me/lebot/axel/cv/MainActivity.java +++ /dev/null @@ -1,14 +0,0 @@ -package me.lebot.axel.cv; - -import android.os.Bundle; - -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/android/app/src/main/kotlin/me/lebot/axel/social_cv_client_flutter/MainActivity.kt b/android/app/src/main/kotlin/me/lebot/axel/social_cv_client_flutter/MainActivity.kt new file mode 100644 index 0000000..080b341 --- /dev/null +++ b/android/app/src/main/kotlin/me/lebot/axel/social_cv_client_flutter/MainActivity.kt @@ -0,0 +1,6 @@ +package me.lebot.axel.social_cv_client_flutter + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 00fa441..322503e 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,8 +1,18 @@ + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..aba602b --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/assets/images/login_logo.png b/assets/images/login_logo.png new file mode 100644 index 0000000..b5ccd30 Binary files /dev/null and b/assets/images/login_logo.png differ diff --git a/config.json.dist b/config.json.dist new file mode 100644 index 0000000..1c7ee7d --- /dev/null +++ b/config.json.dist @@ -0,0 +1,5 @@ +{ + "apiServerUrl": "", + "clientId": "", + "clientSecret": "" +} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/bloc.dart b/lib/bloc.dart new file mode 100644 index 0000000..a67a23f --- /dev/null +++ b/lib/bloc.dart @@ -0,0 +1,24 @@ +/// Blocs + Events + States +export 'package:social_cv_client_flutter/src/bloc/blocs/application/application.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/authentication/authentication.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/configuration/configuration.dart'; + +/// Element Blocs + Event + States +export 'package:social_cv_client_flutter/src/bloc/blocs/element/element.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/element_list/element_list.dart'; + +/// Element List Blocs + Events + States +export 'package:social_cv_client_flutter/src/bloc/blocs/entry/entry.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/entry_list/entry_list.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/group/group.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/group_list/group_list.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/identity/identity.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/login/login.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/part/part.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/part_list/part_list.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/profile/profile.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/profile_list/profile_list.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/register/register.dart'; +export 'package:social_cv_client_flutter/src/bloc/blocs/user/user.dart'; + +//export 'package:social_cv_client_flutter/src/bloc/blocs/user_list/user_list.dart'; diff --git a/lib/data.dart b/lib/data.dart new file mode 100644 index 0000000..ff3be7d --- /dev/null +++ b/lib/data.dart @@ -0,0 +1,82 @@ +export 'package:social_cv_client_flutter/src/data/cache_model.dart'; + +/// ---------------------------------------------------------------------------- +/// Exceptions +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/exceptions/api_exceptions.dart'; + +/// ---------------------------------------------------------------------------- +/// Managers +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/managers/api_interceptor.dart'; +export 'package:social_cv_client_flutter/src/data/managers/cv_api_manager.dart'; + +/// ---------------------------------------------------------------------------- +/// Models +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/models/element_model.dart'; +export 'package:social_cv_client_flutter/src/data/models/entry_model.dart'; +export 'package:social_cv_client_flutter/src/data/models/envelop_models.dart'; +export 'package:social_cv_client_flutter/src/data/models/group_model.dart'; +export 'package:social_cv_client_flutter/src/data/models/part_model.dart'; +export 'package:social_cv_client_flutter/src/data/models/profile_model.dart'; +export 'package:social_cv_client_flutter/src/data/models/user_model.dart'; + +/// ---------------------------------------------------------------------------- +/// Repositories +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/repositories/app_prefs_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/auth_info_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/entry_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/group_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/identity_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/part_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/profile_repository.dart'; +export 'package:social_cv_client_flutter/src/data/repositories/user_repository.dart'; +export 'package:social_cv_client_flutter/src/data/stores/app_prefs_data_store/app_prefs_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/app_prefs_data_store/app_prefs_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/auth_info_data_store/auth_info_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/auth_info_data_store/auth_info_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/entry_data_store/entry_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/entry_data_store/entry_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/entry_data_store/entry_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/entry_data_store/memory_entry_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/group_date_store/group_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/group_date_store/group_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/group_date_store/memory_group_data_store.dart'; + +/// ---------------------------------------------------------------------------- +/// Data Stores +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/stores/identity_data_store/identity_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/identity_data_store/identity_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/identity_data_store/memory_identity_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/part_date_store/memory_part_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/part_date_store/part_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/part_date_store/part_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/profile_date_store/memory_profile_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/profile_date_store/profile_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/profile_date_store/profile_data_store_factory.dart'; +export 'package:social_cv_client_flutter/src/data/stores/user_data_store/memory_user_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/user_data_store/user_data_store.dart'; +export 'package:social_cv_client_flutter/src/data/stores/user_data_store/user_data_store_factory.dart'; + +/// ---------------------------------------------------------------------------- +/// Data Stores +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/utils/utils.dart'; + + +/// ---------------------------------------------------------------------------- +/// Managers +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/data/managers/app_shared_preferences_manager.dart'; +export 'package:social_cv_client_flutter/src/data/managers/auth_shared_preferences_manager.dart'; +export 'package:social_cv_client_flutter/src/data/managers/config_assets_manager.dart'; diff --git a/lib/domain.dart b/lib/domain.dart new file mode 100644 index 0000000..5372519 --- /dev/null +++ b/lib/domain.dart @@ -0,0 +1,45 @@ +/// ---------------------------------------------------------------------------- +/// Entities +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/domain/entities/auth_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/base_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/element_model.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/entry_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/group_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/part_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/profile_entity.dart'; +export 'package:social_cv_client_flutter/src/domain/entities/user_entity.dart'; + +/// ---------------------------------------------------------------------------- +/// Errors +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/domain/errors/commons_errors.dart'; + +/// ---------------------------------------------------------------------------- +/// Exceptions +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/domain/exceptions/app_exceptions.dart'; +export 'package:social_cv_client_flutter/src/domain/exceptions/http_exceptions.dart'; + +/// ---------------------------------------------------------------------------- +/// Repositories +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/domain/repositories/app_preferences_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/auth_info_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/entry_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/group_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/identity_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/part_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/profile_repository.dart'; +export 'package:social_cv_client_flutter/src/domain/repositories/user_repository.dart'; + +/// ---------------------------------------------------------------------------- +/// Services +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/domain/services/cv_auth_service.dart'; +export 'package:social_cv_client_flutter/src/domain/services/foundation_config_service.dart'; diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart new file mode 100644 index 0000000..9f6808e --- /dev/null +++ b/lib/generated_plugin_registrant.dart @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +// ignore: unused_import +import 'dart:ui'; + +import 'package:shared_preferences_web/shared_preferences_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(PluginRegistry registry) { + SharedPreferencesPlugin.registerWith(registry.registrarFor(SharedPreferencesPlugin)); + registry.registerMessageHandler(); +} diff --git a/lib/main.dart b/lib/main.dart index 0a80b16..512b096 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,75 +1,53 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/repositories.dart'; -import 'package:social_cv_client_flutter/src/app.dart'; -import 'package:social_cv_client_flutter/src/repositories/shared_preferences_repository.dart'; -import 'package:social_cv_client_flutter/src/repositories/repositories_provider.dart'; -import 'package:social_cv_client_flutter/src/repositories/local_secrets_repository.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/utils/logging_service.dart'; +import 'package:flutter/rendering.dart'; +import 'package:social_cv_client_flutter/src/presentation/app.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/logger.dart'; -// TODO automatically set this to false for release builds -const DEBUG_MODE = true; +/// TODO: automatically set this to false for release builds +// ignore: constant_identifier_names +const bool DEBUG_MODE = true; +// ignore: constant_identifier_names +const bool DEBUG_PAINT_SIZE = false; + +FutureOr main() async { + String _tag = '$main'; -Future main() async { /// SystemChrome.setPreferredOrientations( /// [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); void run() async { - initLogger(package: "CV App"); - - SecretsRepository secretsRepository = LocalSecretsRepository(); - PreferencesRepository preferencesRepository = SharedPreferencesRepository(); - - CVClient cvClient = CVClientImpl( - accessToken: await preferencesRepository.getAccessToken(), - refreshToken: await preferencesRepository.getRefreshToken(), - ); - CVCache cvCache = CVCacheImpl(); - - CVRepository cvRepository = CVRepositoryImpl( - client: cvClient, - cache: cvCache, - ); - - runApp( - RepositoriesProvider( - cvRepository: cvRepository, - preferencesRepository: preferencesRepository, - secretsRepository: secretsRepository, - child: CVApp(), - ), - ); + runApp(ConfigWrapperApp()); } + FlutterError.onError = globalErrorHandler; + if (DEBUG_MODE) { + debugPaintSizeEnabled = DEBUG_PAINT_SIZE; run(); } else { - FlutterError.onError = globalErrorHandler; runZoned(run, onError: globalErrorHandler); } } -/// /// Global error handler. Show stack trace -/// void globalErrorHandler(details) { - String stackTrace; + StackTrace stackTrace; if (details is FlutterErrorDetails) { if (details.exception is Error) { - stackTrace = details.stack.toString(); + stackTrace = details.stack; } } else if (details is Error) { - stackTrace = details.stackTrace.toString(); + stackTrace = details.stackTrace; } else { + Logger.fatal( + '${details.runtimeType}', + errorCode: ErrorCodes.UNHANDLED_EXCEPTION, + ); throw details; } - LoggingService.fatal( - details.toString(), - errorCode: ErrorCodes.UNHANDLED_EXCEPTION, - stackTrace: stackTrace, - ); + Logger.error('${details.runtimeType}', stackTrace: stackTrace); } diff --git a/lib/presentation.dart b/lib/presentation.dart new file mode 100644 index 0000000..15abd7a --- /dev/null +++ b/lib/presentation.dart @@ -0,0 +1,102 @@ +/// ---------------------------------------------------------------------------- +/// Libs +/// ---------------------------------------------------------------------------- + +export 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +/// ---------------------------------------------------------------------------- +/// Mappers +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/mappers/model_mapper.dart'; + +/// ---------------------------------------------------------------------------- +/// View Models +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/models/api_models.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/cursor_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/element_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/entry_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/group_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/part_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/profile_model.dart'; +export 'package:social_cv_client_flutter/src/presentation/models/user_model.dart'; + +/// ---------------------------------------------------------------------------- +/// Commons +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/commons/api_values.dart'; +export 'package:social_cv_client_flutter/src/presentation/commons/assets.dart'; +export 'package:social_cv_client_flutter/src/presentation/commons/paths.dart'; +export 'package:social_cv_client_flutter/src/presentation/commons/styles.dart'; +export 'package:social_cv_client_flutter/src/presentation/commons/tags.dart'; + +/// ---------------------------------------------------------------------------- +/// Localizations +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; + +/// ---------------------------------------------------------------------------- +/// Pages +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/pages/account_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/auth_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/elements/entry_profile_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/elements/group_profile_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/elements/part_profile_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/elements/profile_profile_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/home_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/main_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/search_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/search_page.dart'; +export 'package:social_cv_client_flutter/src/presentation/pages/settings_page.dart'; + +/// ---------------------------------------------------------------------------- +/// Utils +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/router.dart'; +export 'package:social_cv_client_flutter/src/presentation/utils/logger.dart'; +export 'package:social_cv_client_flutter/src/presentation/utils/navigation.dart'; +export 'package:social_cv_client_flutter/src/presentation/utils/translate_error.dart'; +export 'package:social_cv_client_flutter/src/presentation/utils/utils.dart'; + +/// ---------------------------------------------------------------------------- +/// Widget +/// ---------------------------------------------------------------------------- + +export 'package:social_cv_client_flutter/src/presentation/widgets/account_tile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/arc_banner_image_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/bubble_indication_painter.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/entry_list_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/entry_list_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/entry_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/entry_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/group_list_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/group_list_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/group_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/group_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_list_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/profile_list_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/profile_tile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/elements/profile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/error_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/initial_circle_avatar_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/loading_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/login_form_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/menu_bottom_sheet_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/menu_button_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/profile_image_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/register_form_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/repository_provider.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/sort_box_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/sort_dialog_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/sort_list_tile_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/splash_widget.dart'; +export 'package:social_cv_client_flutter/src/presentation/widgets/theme_switch_tile_widget.dart'; diff --git a/lib/src/app.dart b/lib/src/app.dart deleted file mode 100644 index 84bd603..0000000 --- a/lib/src/app.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/blocs/main_bloc.dart'; -import 'package:social_cv_client_flutter/src/commons/colors.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/pages/main_page.dart'; -import 'package:social_cv_client_flutter/src/repositories/repositories_provider.dart'; -import 'package:social_cv_client_flutter/src/routes.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; - -class CVApp extends StatelessWidget { - const CVApp({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - logger.info('Building App'); - - RepositoriesProvider repositories = RepositoriesProvider.of(context); - - return BlocProvider( - bloc: ApplicationBloc( - preferencesRepository: repositories.preferencesRepository, - ), - child: BlocProvider( - bloc: AccountBloc( - cvRepository: repositories.cvRepository, - preferencesRepository: repositories.preferencesRepository, - secretRepository: repositories.secretsRepository, - ), - child: _CVThemedApp(), - ), - ); - } -} - -class _CVThemedApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - RepositoriesProvider repositories = RepositoriesProvider.of(context); - - BlocProvider _mainPageProvider = BlocProvider( - bloc: MainBloc(), - child: MainPage(), - ); - - ///Routes - Routes routes = Routes( - mainPageProvider: _mainPageProvider, - cvRepository: repositories.cvRepository, - preferencesRepository: repositories.preferencesRepository, - secretsRepository: repositories.secretsRepository, - ); - - ApplicationBloc _appBloc = BlocProvider.of(context); - - return StreamBuilder( - stream: _appBloc.themeStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - return MaterialApp( - onGenerateTitle: (BuildContext context) => - CVLocalizations.of(context).appName, - theme: _buildCVTheme(snapshot.data), - home: _mainPageProvider, - onGenerateRoute: routes.router.generator, - - ///Use Fluro routes - localizationsDelegates: [ - const CVLocalizationsDelegate(), - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en'), - const Locale('fr'), - ], - debugShowCheckedModeBanner: false, -// showSemanticsDebugger: true, - ); - }); - } - - ThemeData _buildCVTheme(String theme) { - ThemeData base; - if (theme != ThemeType.DARK) - base = ThemeData.light(); - else { - base = ThemeData.dark(); - } - - return base.copyWith( - primaryColor: AppColors.kCVPrimaryColor, - primaryColorLight: AppColors.kCVPrimaryColorLight, - primaryColorDark: AppColors.kCVPrimaryColorDark, - accentColor: AppColors.kCVAccentColor, - buttonColor: (theme != ThemeType.DARK) - ? AppColors.kCVWhite - : AppColors.kCVPrimaryColorDark, - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder(), - ), - textTheme: _buildCVTextTheme(base.textTheme), - primaryTextTheme: _buildCVTextTheme(base.primaryTextTheme), - accentTextTheme: _buildCVTextTheme(base.accentTextTheme), - ); - } - - TextTheme _buildCVTextTheme(TextTheme base) { - return base.apply( - fontFamily: 'Google Sans', - ); - } -} diff --git a/lib/src/bloc/blocs/application/application.dart b/lib/src/bloc/blocs/application/application.dart new file mode 100644 index 0000000..57124f2 --- /dev/null +++ b/lib/src/bloc/blocs/application/application.dart @@ -0,0 +1,3 @@ +export './application_bloc.dart'; +export './application_event.dart'; +export './application_state.dart'; diff --git a/lib/src/bloc/blocs/application/application_bloc.dart b/lib/src/bloc/blocs/application/application_bloc.dart new file mode 100644 index 0000000..e9174fb --- /dev/null +++ b/lib/src/bloc/blocs/application/application_bloc.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Application behaviors +/// Can manage theme +class AppBloc extends Bloc { + final String _tag = '$AppBloc'; + + final AppPrefsRepository appPreferencesRepository; + + AppBloc({ + @required this.appPreferencesRepository, + }) : assert( + appPreferencesRepository != null, + 'No $AppPrefsRepository given', + ), + super(); + + @override + AppState get initialState => AppUninitialized(); + + @override + Stream mapEventToState(AppEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is AppConfigured) { + yield* _mapAppConfiguredToState(event); + } else if (event is AppThemeChanged) { + yield* _mapAppThemeChangedToState(event); + } + } + + /// ----------------------------------------------------------------------- + /// All Event map to State + /// ----------------------------------------------------------------------- + + /// Map [AppThemeChanged] to [AppState] + /// + /// ```dart + /// yield* _mapAppThemeChangedToState(event); + /// ``` + Stream _mapAppThemeChangedToState(AppThemeChanged event) async* { + try { + yield AppLoading(); + await appPreferencesRepository.toggleDarkMode(event.darkMode); + yield AppInitialized(darkMode: event.darkMode); + } catch (error) { + yield AppFailure(error: error); + } + } + + /// Map [AppConfigured] to [AppState] + /// + /// ```dart + /// yield* _mapAppConfiguredToState(event); + /// ``` + Stream _mapAppConfiguredToState(AppConfigured event) async* { + try { + yield AppLoading(); + final darkMode = await appPreferencesRepository.getDarkMode() ?? false; + yield AppInitialized(darkMode: darkMode); + } catch (error) { + yield AppFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/application/application_event.dart b/lib/src/bloc/blocs/application/application_event.dart new file mode 100644 index 0000000..e20c3ff --- /dev/null +++ b/lib/src/bloc/blocs/application/application_event.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +/// [AppEvent] that must be dispatch to [AppBloc] +abstract class AppEvent extends Equatable { + AppEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class AppConfigured extends AppEvent {} + +class AppThemeChanged extends AppEvent { + final bool darkMode; + + AppThemeChanged({@required this.darkMode}) : super([darkMode]); + + @override + String toString() => '$runtimeType{ ' + 'darkMode: $darkMode' + ' }'; +} diff --git a/lib/src/bloc/blocs/application/application_state.dart b/lib/src/bloc/blocs/application/application_state.dart new file mode 100644 index 0000000..8fa1ca6 --- /dev/null +++ b/lib/src/bloc/blocs/application/application_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract class AppState extends Equatable { + AppState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class AppUninitialized extends AppState {} + +class AppInitialized extends AppState { + final bool darkMode; + + AppInitialized({this.darkMode = false}) : super([darkMode]); + + @override + String toString() => '$runtimeType{ ' + 'darkMode: $darkMode' + ' }'; +} + +class AppFailure extends AppState { + final dynamic error; + + AppFailure({@required this.error}) + : assert(error != null, 'No error given'), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} + +class AppLoading extends AppState {} diff --git a/lib/src/bloc/blocs/authentication/authentication.dart b/lib/src/bloc/blocs/authentication/authentication.dart new file mode 100644 index 0000000..0383408 --- /dev/null +++ b/lib/src/bloc/blocs/authentication/authentication.dart @@ -0,0 +1,3 @@ +export './authentication_bloc.dart'; +export './authentication_event.dart'; +export './authentication_state.dart'; diff --git a/lib/src/bloc/blocs/authentication/authentication_bloc.dart b/lib/src/bloc/blocs/authentication/authentication_bloc.dart new file mode 100644 index 0000000..e38a1bb --- /dev/null +++ b/lib/src/bloc/blocs/authentication/authentication_bloc.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Authentication +class AuthenticationBloc + extends Bloc { + final String _tag = '$AuthenticationBloc'; + + final CVAuthService cvAuthService; + final AuthInfoRepository authInfoRepository; + final LoginBloc loginBloc; + final RegisterBloc registerBloc; + + StreamSubscription registerBlocSubscription; + StreamSubscription loginBlocSubscription; + + AuthenticationBloc({ + @required this.cvAuthService, + @required this.authInfoRepository, + @required this.loginBloc, + @required this.registerBloc, + }) : assert(cvAuthService != null, 'No $CVAuthService given'), + assert(authInfoRepository != null, 'No $AppPrefsRepository given'), + assert(loginBloc != null, 'No $LoginBloc given'), + assert(registerBloc != null, 'No $RegisterBloc given'), + super() { + loginBlocSubscription = loginBloc.state.listen((state) { + if (state is LoginSucceed) { + dispatch(LoggedIn( + accessToken: state.accessToken, + accessTokenExpiration: state.accessTokenExpiration, + refreshToken: state.refreshToken, + )); + } + }); + + registerBlocSubscription = registerBloc.state.listen((state) { + if (state is RegisterSucceed) { + dispatch(LoggedIn( + accessToken: state.accessToken, + accessTokenExpiration: state.accessTokenExpiration, + refreshToken: state.refreshToken, + )); + } + }); + } + + @override + void dispose() { + loginBlocSubscription.cancel(); + registerBlocSubscription.cancel(); + super.dispose(); + } + + @override + AuthenticationState get initialState => AuthenticationUninitialized(); + + @override + Stream mapEventToState( + AuthenticationEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is AppStarted) { + yield* _mapAppStartedToState(event); + } else if (event is LoggedIn) { + yield* _mapLoggedInToState(event); + } else if (event is LoggedOut) { + yield* _mapLoggedOutToState(event); + } + } + + /// ----------------------------------------------------------------------- + /// All Event map to State + /// ----------------------------------------------------------------------- + + /// Map [AppStarted] to [AuthenticationState] + /// + /// ```dart + /// yield* _mapAppStartedToState(event); + /// ``` + Stream _mapAppStartedToState(AppStarted event) async* { + try { + final token = await authInfoRepository.getAccessToken(); + + /// TODO: Check access token expiration and fetch new access token with refresh token + /// TODO: Check refresh token expiration, if it's expired set state to Unauthenticated + + if (token != null && token?.length > 0) { + yield AuthenticationAuthenticated(); + } else { + yield AuthenticationUnauthenticated(); + } + } catch (error) { + yield AuthenticationFailed(error: error); + } + } + + /// Map [LoggedIn] to [AuthenticationState] + /// + /// ```dart + /// yield* _mapLoggedInToState(event); + /// ``` + Stream _mapLoggedInToState(LoggedIn event) async* { + yield AuthenticationLoading(); + await authInfoRepository.setAccessToken(event.accessToken); + await authInfoRepository + .setAccessTokenExpiration(event.accessTokenExpiration); + await authInfoRepository.setRefreshToken(event.refreshToken); + yield AuthenticationAuthenticated(); + } + + /// Map [LoggedIn] to [AuthenticationState] + /// + /// ```dart + /// yield* _mapLoggedInToState(event); + /// ``` + Stream _mapLoggedOutToState(LoggedOut event) async* { + yield AuthenticationLoading(); + await cvAuthService.logout(); + await authInfoRepository.deleteAccessToken(); + await authInfoRepository.deleteAccessTokenExpiration(); + await authInfoRepository.deleteRefreshToken(); + await authInfoRepository.deleteRefreshTokenExpiration(); + yield AuthenticationUnauthenticated(); + } +} diff --git a/lib/src/bloc/blocs/authentication/authentication_event.dart b/lib/src/bloc/blocs/authentication/authentication_event.dart new file mode 100644 index 0000000..f908226 --- /dev/null +++ b/lib/src/bloc/blocs/authentication/authentication_event.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +/// [AuthenticationEvent] that must be dispatch to [AuthenticationBloc] +abstract class AuthenticationEvent extends Equatable { + AuthenticationEvent([List props = const []]) : super(props); +} + +/// Use [AppStarted] to begin auth process on startup +class AppStarted extends AuthenticationEvent {} + +/// Use [LoggedIn] to inform that user just logged in +class LoggedIn extends AuthenticationEvent { + final String accessToken; + final DateTime accessTokenExpiration; + final String refreshToken; + + LoggedIn({ + @required this.accessToken, + @required this.accessTokenExpiration, + @required this.refreshToken, + }) : super([ + accessToken, + accessTokenExpiration, + refreshToken, + ]); + + @override + String toString() => '$runtimeType{ ' + 'accessToken: $accessToken, ' + 'accessTokenExpiration: $accessTokenExpiration, ' + 'refreshToken: $refreshToken, ' + ' }'; +} + +/// Use [LoggedOut] to request logout +class LoggedOut extends AuthenticationEvent {} diff --git a/lib/src/bloc/blocs/authentication/authentication_state.dart b/lib/src/bloc/blocs/authentication/authentication_state.dart new file mode 100644 index 0000000..d2b9a2e --- /dev/null +++ b/lib/src/bloc/blocs/authentication/authentication_state.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract class AuthenticationState extends Equatable { + AuthenticationState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class AuthenticationUninitialized extends AuthenticationState {} + +class AuthenticationAuthenticated extends AuthenticationState {} + +class AuthenticationUnauthenticated extends AuthenticationState {} + +class AuthenticationLoading extends AuthenticationState {} + +class AuthenticationFailed extends AuthenticationState { + final dynamic error; + + AuthenticationFailed({@required this.error}) + : assert(error != null, 'No error given'), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/configuration/configuration.dart b/lib/src/bloc/blocs/configuration/configuration.dart new file mode 100644 index 0000000..5a48431 --- /dev/null +++ b/lib/src/bloc/blocs/configuration/configuration.dart @@ -0,0 +1,3 @@ +export './configuration_bloc.dart'; +export './configuration_event.dart'; +export './configuration_state.dart'; diff --git a/lib/src/bloc/blocs/configuration/configuration_bloc.dart b/lib/src/bloc/blocs/configuration/configuration_bloc.dart new file mode 100644 index 0000000..b8b8d2c --- /dev/null +++ b/lib/src/bloc/blocs/configuration/configuration_bloc.dart @@ -0,0 +1,178 @@ +import 'package:bloc/bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ConfigurationBloc extends Bloc { + final String _tag = '$ConfigurationBloc'; + + ConfigurationBloc() : super(); + + /// Services + FoundationConfigService _foundationConfigService; + CVAuthService _cvAuthService; + + /// Repositories + AuthInfoRepository _authInfoRepository; + AppPrefsRepository _appPrefsRepository; + + /// Entities Repositories + IdentityRepository _identityRepository; + UserRepository _userRepository; + ProfileRepository _profileRepository; + PartRepository _partRepository; + GroupRepository _groupRepository; + EntryRepository _entryRepository; + + @override + ConfigurationState get initialState => ConfigLoading(); + + @override + Stream mapEventToState(ConfigurationEvent event) async* { + if (event is AppLaunched) { + yield* _mapAppLaunchedEventToState(); + } + } + + /// ----------------------------------------------------------------------- + /// All Event map to State + /// ----------------------------------------------------------------------- + + Stream _mapAppLaunchedEventToState() async* { + try { + yield ConfigLoading(); + + _foundationConfigService = ConfigAssetsManager(); + + final AuthInfoDataStore diskAuthInfoDataStore = + AuthSharedPreferencesManager(); + + final apiInterceptor = ApiInterceptor( + accessToken: await diskAuthInfoDataStore.getAccessToken(), + refreshToken: await diskAuthInfoDataStore.getRefreshToken(), + ); + + final CVApiManager cvApiManager = CVApiManager( + apiBaseUrl: await _foundationConfigService.getApiServerUrl(), + tokenInterceptor: apiInterceptor, + ); + + _cvAuthService = cvApiManager; + + // Data Stores + + final AppPrefsDataStore diskAppPrefsDataStore = AppPrefsManager(); + + final IdentityDataStore memoryIdentityDataStore = + MemoryIdentityDataStore(); + final IdentityDataStore cloudIdentityDataStore = cvApiManager; + + final ProfileDataStore memoryProfileDataStore = MemoryProfileDataStore(); + final ProfileDataStore cloudProfileDataStore = cvApiManager; + + final UserDataStore memoryUserDataStore = MemoryUserDataStore(); + final UserDataStore cloudUserDataStore = cvApiManager; + + final PartDataStore memoryPartDataStore = MemoryPartDataStore(); + final PartDataStore cloudPartDataStore = cvApiManager; + + final GroupDataStore memoryGroupDataStore = MemoryGroupDataStore(); + final GroupDataStore cloudGroupDataStore = cvApiManager; + + final EntryDataStore memoryEntryDataStore = MemoryEntryDataStore(); + final EntryDataStore cloudEntryDataStore = cvApiManager; + + // Data Store Factories + + final appPrefsDataStoreFactory = AppPrefsDataStoreFactory( + diskDataStore: diskAppPrefsDataStore, + ); + + final authInfoDataStoreFactory = AuthInfoDataStoreFactory( + diskDataStore: diskAuthInfoDataStore, + ); + + final identityDataStoreFactory = IdentityDataStoreFactory( + memoryDataStore: memoryIdentityDataStore, + cloudDataStore: cloudIdentityDataStore, + ); + + final userDataStoreFactory = UserDataStoreFactory( + memoryDataStore: memoryUserDataStore, + cloudDataStore: cloudUserDataStore, + ); + + final profileDataStoreFactory = ProfileDataStoreFactory( + memoryDataStore: memoryProfileDataStore, + cloudDataStore: cloudProfileDataStore, + ); + + final partDataStoreFactory = PartDataStoreFactory( + memoryDataStore: memoryPartDataStore, + cloudDataStore: cloudPartDataStore, + ); + + final groupDataStoreFactory = GroupDataStoreFactory( + memoryDataStore: memoryGroupDataStore, + cloudDataStore: cloudGroupDataStore, + ); + + final entryDataStoreFactory = EntryDataStoreFactory( + memoryDataStore: memoryEntryDataStore, + cloudDataStore: cloudEntryDataStore, + ); + + // Repositories + + _appPrefsRepository = ImplAppPrefsRepository( + factory: appPrefsDataStoreFactory, + ); + + _authInfoRepository = ImplAuthInfoRepository( + factory: authInfoDataStoreFactory, + ); + + _identityRepository = ImplIdentityRepository( + factory: identityDataStoreFactory, + ); + + _userRepository = ImplUserRepository( + factory: userDataStoreFactory, + ); + + _profileRepository = ImplProfileRepository( + factory: profileDataStoreFactory, + ); + + _partRepository = ImplPartRepository( + factory: partDataStoreFactory, + ); + + _groupRepository = ImplGroupRepository( + factory: groupDataStoreFactory, + ); + + _entryRepository = ImplEntryRepository( + factory: entryDataStoreFactory, + ); + + // Return config + + yield ConfigLoaded( + cvAuthService: _cvAuthService, + authInfoRepository: _authInfoRepository, + appPrefsRepository: _appPrefsRepository, + identityRepository: _identityRepository, + userRepository: _userRepository, + profileRepository: _profileRepository, + partRepository: _partRepository, + groupRepository: _groupRepository, + entryRepository: _entryRepository, + ); + } catch (error, stacktrace) { + Logger.error('${error.runtimeType}', stackTrace: stacktrace); + yield ConfigFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/configuration/configuration_event.dart b/lib/src/bloc/blocs/configuration/configuration_event.dart new file mode 100644 index 0000000..ae9b059 --- /dev/null +++ b/lib/src/bloc/blocs/configuration/configuration_event.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; + +/// [ConfigurationEvent] that must be dispatch to [AppBloc] +abstract class ConfigurationEvent extends Equatable { + ConfigurationEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class AppLaunched extends ConfigurationEvent {} diff --git a/lib/src/bloc/blocs/configuration/configuration_state.dart b/lib/src/bloc/blocs/configuration/configuration_state.dart new file mode 100644 index 0000000..cefdd55 --- /dev/null +++ b/lib/src/bloc/blocs/configuration/configuration_state.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class ConfigurationState extends Equatable { + ConfigurationState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class ConfigLoading extends ConfigurationState {} + +class ConfigLoaded extends ConfigurationState { + final CVAuthService cvAuthService; + + final AuthInfoRepository authInfoRepository; + final AppPrefsRepository appPrefsRepository; + + final IdentityRepository identityRepository; + final UserRepository userRepository; + final ProfileRepository profileRepository; + final PartRepository partRepository; + final GroupRepository groupRepository; + final EntryRepository entryRepository; + + ConfigLoaded({ + @required this.cvAuthService, + @required this.authInfoRepository, + @required this.appPrefsRepository, + @required this.identityRepository, + @required this.userRepository, + @required this.profileRepository, + @required this.partRepository, + @required this.groupRepository, + @required this.entryRepository, + }) : assert(cvAuthService != null, 'No $CVAuthService given'), + assert(authInfoRepository != null, 'No $AuthInfoRepository given'), + assert(appPrefsRepository != null, 'No $AppPrefsRepository given'), + assert(identityRepository != null, 'No $IdentityRepository given'), + assert(userRepository != null, 'No $UserRepository given'), + assert(profileRepository != null, 'No $ProfileRepository given'), + assert(partRepository != null, 'No $PartRepository given'), + assert(groupRepository != null, 'No $GroupRepository given'), + assert(entryRepository != null, 'No $EntryRepository given'), + super([ + cvAuthService, + authInfoRepository, + appPrefsRepository, + identityRepository, + userRepository, + profileRepository, + partRepository, + groupRepository, + entryRepository, + ]); +} + +class ConfigFailure extends ConfigurationState { + final dynamic error; + + ConfigFailure({@required this.error}) + : assert(error != null), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/element/element.dart b/lib/src/bloc/blocs/element/element.dart new file mode 100644 index 0000000..a927d20 --- /dev/null +++ b/lib/src/bloc/blocs/element/element.dart @@ -0,0 +1,3 @@ +export './element_bloc.dart'; +export './element_event.dart'; +export './element_state.dart'; diff --git a/lib/src/bloc/blocs/element/element_bloc.dart b/lib/src/bloc/blocs/element/element_bloc.dart new file mode 100644 index 0000000..10bad00 --- /dev/null +++ b/lib/src/bloc/blocs/element/element_bloc.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for profile elements +abstract class ElementBloc + extends Bloc { + final String _tag = '$ElementBloc<$T,$R,$E,$S>'; + + final R repository; + + T element; + + ElementBloc({@required this.repository}) + : assert(repository != null, 'No $R given'), + super(); +} diff --git a/lib/src/bloc/blocs/element/element_event.dart b/lib/src/bloc/blocs/element/element_event.dart new file mode 100644 index 0000000..ba5ee14 --- /dev/null +++ b/lib/src/bloc/blocs/element/element_event.dart @@ -0,0 +1,17 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +mixin ElementInitialized { + String elementId; + T element; + + @override + String toString() => '$runtimeType{ ' + 'id: $elementId, ' + 'element: $element' + ' }'; +} + +mixin ElementRefresh { + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/bloc/blocs/element/element_state.dart b/lib/src/bloc/blocs/element/element_state.dart new file mode 100644 index 0000000..1824ab9 --- /dev/null +++ b/lib/src/bloc/blocs/element/element_state.dart @@ -0,0 +1,29 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +mixin ElementUninitialized { + @override + String toString() => '$runtimeType{}'; +} + +mixin ElementLoading { + @override + String toString() => '$runtimeType{}'; +} + +mixin ElementLoaded { + T element; + + @override + String toString() => '$runtimeType{ ' + 'element: $element' + ' }'; +} + +mixin ElementFailure { + dynamic error; + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/element_list/element_list.dart b/lib/src/bloc/blocs/element_list/element_list.dart new file mode 100644 index 0000000..f84b7db --- /dev/null +++ b/lib/src/bloc/blocs/element_list/element_list.dart @@ -0,0 +1,3 @@ +export './element_list_bloc.dart'; +export './element_list_event.dart'; +export './element_list_state.dart'; diff --git a/lib/src/bloc/blocs/element_list/element_list_bloc.dart b/lib/src/bloc/blocs/element_list/element_list_bloc.dart new file mode 100644 index 0000000..21c9f1c --- /dev/null +++ b/lib/src/bloc/blocs/element_list/element_list_bloc.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Business Logic Component for profile element list +abstract class ElementListBloc + extends Bloc { + final String _tag = '$ElementListBloc<$T>'; + + final R repository; + + String parentId; + List elements; + String ownerId; + + Cursor cursor; + + /// TODO: Add filter + /// TODO: Add sort + + ElementListBloc({@required this.repository}) + : assert(repository != null, 'No $R given'), + super(); +} diff --git a/lib/src/bloc/blocs/element_list/element_list_event.dart b/lib/src/bloc/blocs/element_list/element_list_event.dart new file mode 100644 index 0000000..c5ead90 --- /dev/null +++ b/lib/src/bloc/blocs/element_list/element_list_event.dart @@ -0,0 +1,29 @@ +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +mixin ElementListInitialized { + String parentId; + String ownerId; + Cursor cursor; + + @override + String toString() => '$runtimeType{ ' + 'parentId: $parentId, ' + 'ownerId: $ownerId, ' + 'cursor: $cursor' + ' }'; +} + +mixin ElementListRefresh { + @override + String toString() => '$runtimeType{}'; +} + +mixin ElementListLoadMore { + Cursor cursor; + + @override + String toString() => '$runtimeType{ ' + 'cursor: $cursor' + ' }'; +} diff --git a/lib/src/bloc/blocs/element_list/element_list_state.dart b/lib/src/bloc/blocs/element_list/element_list_state.dart new file mode 100644 index 0000000..2e54afa --- /dev/null +++ b/lib/src/bloc/blocs/element_list/element_list_state.dart @@ -0,0 +1,33 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +mixin ElementListUninitialized { + @override + String toString() => '$runtimeType{}'; +} + +mixin ElementListLoading { + int count; + + @override + String toString() => '$runtimeType{ ' + 'count: $count' + ' }'; +} + +mixin ElementListLoaded { + List elements; + + @override + String toString() => '$runtimeType{ ' + 'elements: $elements' + ' }'; +} + +mixin ElementListFailure { + dynamic error; + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/entry/entry.dart b/lib/src/bloc/blocs/entry/entry.dart new file mode 100644 index 0000000..929412b --- /dev/null +++ b/lib/src/bloc/blocs/entry/entry.dart @@ -0,0 +1,3 @@ +export 'entry_bloc.dart'; +export 'entry_event.dart'; +export 'entry_state.dart'; diff --git a/lib/src/bloc/blocs/entry/entry_bloc.dart b/lib/src/bloc/blocs/entry/entry_bloc.dart new file mode 100644 index 0000000..7e29088 --- /dev/null +++ b/lib/src/bloc/blocs/entry/entry_bloc.dart @@ -0,0 +1,81 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Entry +class EntryBloc + extends ElementBloc { + final String _tag = '$EntryBloc'; + + EntryBloc({@required EntryRepository repository}) + : super(repository: repository); + + /// [_fallBackId] is used if [element] is never assigned and + /// an [EntryRefresh] is dispatched + String _fallBackId; + + @override + EntryState get initialState => EntryUninitialized(); + + @override + Stream mapEventToState(EntryEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is EntryInitialized) { + yield* _mapInitializedEventToState(event); + } else if (event is EntryRefresh) { + yield* _mapRefreshEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [EntryInitialized] to [EntryState] + /// + /// ```dart + /// yield* _mapInitializedEventToState(event); + /// ``` + Stream _mapInitializedEventToState( + EntryInitialized event) async* { + print('$_tag:_mapInitializedEventToState($event)'); + try { + yield EntryLoading(); + + if (event.elementId != null) { + _fallBackId = event.elementId; + element = await repository.getById(event.elementId); + } else if (event.element != null) { + _fallBackId = event.element.id; + element = event.element; + } + + yield EntryLoaded(entry: element); + } catch (error) { + yield EntryFailure(error: error); + } + } + + /// Map [EntryRefresh] to [EntryState] + /// + /// ```dart + /// yield* _mapRefreshEventToState(event); + /// ``` + Stream _mapRefreshEventToState(EntryRefresh event) async* { + print('$_tag:_mapRefreshEventToState($event)'); + try { + yield EntryLoading(); + + element = await repository.getById( + element?.id ?? _fallBackId, + force: true, + ); + + _fallBackId = element.id; + + yield EntryLoaded(entry: element); + } catch (error) { + yield EntryFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/entry/entry_event.dart b/lib/src/bloc/blocs/entry/entry_event.dart new file mode 100644 index 0000000..a5608e1 --- /dev/null +++ b/lib/src/bloc/blocs/entry/entry_event.dart @@ -0,0 +1,35 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// [EntryEvent] that must be dispatch to [EntryBloc] +abstract class EntryEvent extends Equatable { + EntryEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class EntryInitialized extends EntryEvent with ElementInitialized { + EntryInitialized({String entryId, EntryEntity entry}) + : assert( + entryId != null && entry == null, + '$EntryInitialized must be created with an $EntryEntity or its ID', + ), + assert( + entryId == null && entry != null, + '$EntryInitialized must be created with an $EntryEntity or its ID', + ), + super([entryId, entry]) { + elementId = entryId; + element = entry; + } + + @override + String toString() => '$runtimeType{ ' + 'entryId: $elementId, ' + 'element: $element' + ' }'; +} + +class EntryRefresh extends EntryEvent with ElementRefresh {} diff --git a/lib/src/bloc/blocs/entry/entry_state.dart b/lib/src/bloc/blocs/entry/entry_state.dart new file mode 100644 index 0000000..c19a5c1 --- /dev/null +++ b/lib/src/bloc/blocs/entry/entry_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class EntryState extends Equatable { + EntryState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class EntryUninitialized extends EntryState + with ElementUninitialized {} + +class EntryLoading extends EntryState with ElementLoading {} + +class EntryLoaded extends EntryState with ElementLoaded { + EntryLoaded({EntryEntity entry}) : super([entry]) { + element = entry; + } + + @override + String toString() { + return '$runtimeType{ ' + 'entry: $element' + ' }'; + } +} + +class EntryFailure extends EntryState with ElementFailure { + EntryFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/entry_list/entry_list.dart b/lib/src/bloc/blocs/entry_list/entry_list.dart new file mode 100644 index 0000000..f229303 --- /dev/null +++ b/lib/src/bloc/blocs/entry_list/entry_list.dart @@ -0,0 +1,3 @@ +export './entry_list_bloc.dart'; +export './entry_list_event.dart'; +export './entry_list_state.dart'; diff --git a/lib/src/bloc/blocs/entry_list/entry_list_bloc.dart b/lib/src/bloc/blocs/entry_list/entry_list_bloc.dart new file mode 100644 index 0000000..33c8f70 --- /dev/null +++ b/lib/src/bloc/blocs/entry_list/entry_list_bloc.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Business Logic Component for Entry list +class EntryListBloc extends ElementListBloc { + final String _tag = '$EntryListBloc'; + + EntryListBloc({@required EntryRepository repository}) + : super(repository: repository); + + @override + EntryListState get initialState => EntryListUninitialized(); + + @override + Stream mapEventToState(EntryListEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is EntryListInitialized) { + yield* _mapEntryListInitializedEventToState(event); + } else if (event is EntryListRefresh) { + yield* _mapEntryListRefreshEventToState(event); + } else if (event is EntryListLoadMore) { + yield* _mapEntryListLoadMoreEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [EntryListInitialized] to [EntryListState] + /// + /// ```dart + /// yield* _mapEntryListInitializedEventToState(event); + /// ``` + Stream _mapEntryListInitializedEventToState( + EntryListInitialized event) async* { + print('$_tag:_mapEntryListInitializedEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + + parentId = event.parentId; + ownerId = event.ownerId; + cursor = event.cursor; + + elements = await _getEntries(cursor: event.cursor); + + yield EntryListLoaded(entries: elements); + } catch (error) { + yield EntryListFailure(error: error); + } + } + + /// Map [EntryListRefresh] to [EntryListState] + /// + /// ```dart + /// yield* _mapEntryListRefreshEventToState(event); + /// ``` + Stream _mapEntryListRefreshEventToState( + EntryListRefresh event) async* { + print('$_tag:_mapEntryListRefreshEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + elements = await _getEntries(cursor: cursor); + yield EntryListLoaded(entries: elements); + } catch (error) { + yield EntryListFailure(error: error); + } + } + + /// Map [EntryListLoadMore] to [EntryListState] + /// + /// ```dart + /// yield* _mapEntryListRefreshEventToState(event); + /// ``` + Stream _mapEntryListLoadMoreEventToState( + EntryListLoadMore event) async* { + print('$_tag:_mapEntryListLoadMoreEventToState($event)'); + try { + /// TODO: Add load more indicator stream + + final List entries = await _getEntries( + cursor: event.cursor.copyWith(offset: elements.length), + ); + + /// Append to elements + elements.addAll(entries); + + /// Save cursor limit if use list refreshed + cursor = cursor.copyWith(limit: elements.length); + + yield EntryListLoaded(entries: elements); + } catch (error) { + yield EntryListFailure(error: error); + } + } + + FutureOr> _getEntries({@required Cursor cursor}) async { + print('$_tag:_getEntries({cursor: $cursor})'); + if (parentId != null) { + return await repository.getEntriesFromGroup( + parentId, + cursor: cursor, + ); + } else if (ownerId != null) { + return await repository.getEntriesFromUser( + ownerId, + cursor: cursor, + ); + } else { + return await repository.getList( + cursor: cursor, + ); + } + } +} diff --git a/lib/src/bloc/blocs/entry_list/entry_list_event.dart b/lib/src/bloc/blocs/entry_list/entry_list_event.dart new file mode 100644 index 0000000..22e5e7e --- /dev/null +++ b/lib/src/bloc/blocs/entry_list/entry_list_event.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [EntryListEvent] that must be dispatch to [EntryListBloc] +abstract class EntryListEvent extends Equatable { + EntryListEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class EntryListInitialized extends EntryListEvent + with ElementListInitialized { + EntryListInitialized({ + String parentGroupId, + String ownerId, + Cursor cursor, + }) : assert( + parentGroupId != null && ownerId == null, + '$EntryListInitialized must be created with a parentId or an ownerId', + ), + assert( + parentGroupId == null && ownerId != null, + '$EntryListInitialized must be created with a parentId or an ownerId', + ), + super([parentGroupId, ownerId]) { + this.parentId = parentGroupId; + this.ownerId = ownerId; + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'parentId: $parentId, ' + 'ownerId: $ownerId, ' + 'cursor: $cursor' + ' }'; +} + +class EntryListRefresh extends EntryListEvent + with ElementListRefresh {} + +class EntryListLoadMore extends EntryListEvent + with ElementListLoadMore { + EntryListLoadMore({Cursor cursor}) + : assert(cursor != null), + super([cursor]) { + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'cursor: $cursor' + ' }'; +} diff --git a/lib/src/bloc/blocs/entry_list/entry_list_state.dart b/lib/src/bloc/blocs/entry_list/entry_list_state.dart new file mode 100644 index 0000000..ffef042 --- /dev/null +++ b/lib/src/bloc/blocs/entry_list/entry_list_state.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class EntryListState extends Equatable { + EntryListState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class EntryListUninitialized extends EntryListState + with ElementListUninitialized {} + +class EntryListLoading extends EntryListState + with ElementListLoading { + EntryListLoading({int count = 0}) : super([count]) { + this.count = count; + } + + @override + String toString() => '$runtimeType{ ' + 'count: $count' + ' }'; +} + +class EntryListLoaded extends EntryListState + with ElementListLoaded { + EntryListLoaded({@required List entries}) : super([entries]) { + elements = entries; + } + + @override + String toString() => '$runtimeType{ ' + 'entries: $elements' + ' }'; +} + +class EntryListFailure extends EntryListState + with ElementListFailure { + EntryListFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/group/group.dart b/lib/src/bloc/blocs/group/group.dart new file mode 100644 index 0000000..19c3ef1 --- /dev/null +++ b/lib/src/bloc/blocs/group/group.dart @@ -0,0 +1,3 @@ +export 'group_bloc.dart'; +export 'group_event.dart'; +export 'group_state.dart'; diff --git a/lib/src/bloc/blocs/group/group_bloc.dart b/lib/src/bloc/blocs/group/group_bloc.dart new file mode 100644 index 0000000..c49e91a --- /dev/null +++ b/lib/src/bloc/blocs/group/group_bloc.dart @@ -0,0 +1,83 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Group +class GroupBloc + extends ElementBloc { + final String _tag = '$GroupBloc'; + + GroupBloc({@required GroupRepository repository}) + : super(repository: repository); + + /// [_fallBackId] is used if [element] is never assigned and + /// an [GroupRefresh] is dispatched + String _fallBackId; + + @override + GroupState get initialState => GroupUninitialized(); + + @override + Stream mapEventToState(GroupEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is GroupInitialized) { + yield* _mapInitializedEventToState(event); + } else if (event is GroupRefresh) { + yield* _mapRefreshEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + Stream _mapInitializedEventToState( + GroupInitialized event) async* { + print('$_tag:_mapInitializedEventToState($event)'); + try { + yield GroupLoading(); + + if (event.elementId != null) { + _fallBackId = event.elementId; + element = await repository.getById(event.elementId); + } else if (event.element != null) { + _fallBackId = event.element.id; + element = event.element; + } + + yield GroupLoaded(group: element); + } catch (error) { + yield GroupFailure(error: error); + } + } + + /// Map [GroupRefresh] to [GroupState] + /// + /// ```dart + /// yield* _mapRefreshEventToState(event); + /// ``` + Stream _mapRefreshEventToState(GroupRefresh event) async* { + print('$_tag:_mapRefreshEventToState($event)'); + try { + yield GroupLoading(); + + element = await repository.getById( + element?.id ?? _fallBackId, + force: true, + ); + + _fallBackId = element.id; + + yield GroupLoaded(group: element); + } catch (error) { + yield GroupFailure(error: error); + } + } + + @override + String toString() { + return '$runtimeType{ ' + 'repository: $repository' + ' }'; + } +} diff --git a/lib/src/bloc/blocs/group/group_event.dart b/lib/src/bloc/blocs/group/group_event.dart new file mode 100644 index 0000000..21fe121 --- /dev/null +++ b/lib/src/bloc/blocs/group/group_event.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// [GroupEvent] that must be dispatch to [GroupBloc] + +abstract class GroupEvent extends Equatable { + GroupEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class GroupInitialized extends GroupEvent with ElementInitialized { + GroupInitialized({String groupId, GroupEntity group}) + : assert( + groupId != null && group == null, + '$GroupInitialized must be created with a $GroupEntity or its ID', + ), + assert( + groupId == null && group != null, + '$GroupInitialized must be created with a $GroupEntity or its ID', + ), + super([groupId, group]) { + this.elementId = groupId; + this.element = group; + } + + @override + String toString() => '$runtimeType{ id: $elementId, element: $element }'; +} + +class GroupRefresh extends GroupEvent with ElementRefresh {} diff --git a/lib/src/bloc/blocs/group/group_state.dart b/lib/src/bloc/blocs/group/group_state.dart new file mode 100644 index 0000000..ba68ce5 --- /dev/null +++ b/lib/src/bloc/blocs/group/group_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class GroupState extends Equatable { + GroupState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class GroupUninitialized extends GroupState + with ElementUninitialized {} + +class GroupLoading extends GroupState with ElementLoading {} + +class GroupLoaded extends GroupState with ElementLoaded { + GroupLoaded({GroupEntity group}) : super([group]) { + element = group; + } + + @override + String toString() { + return '$runtimeType{ ' + 'group: $element' + ' }'; + } +} + +class GroupFailure extends GroupState with ElementFailure { + GroupFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType { ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/group_list/group_list.dart b/lib/src/bloc/blocs/group_list/group_list.dart new file mode 100644 index 0000000..33f2978 --- /dev/null +++ b/lib/src/bloc/blocs/group_list/group_list.dart @@ -0,0 +1,3 @@ +export './group_list_bloc.dart'; +export './group_list_event.dart'; +export './group_list_state.dart'; diff --git a/lib/src/bloc/blocs/group_list/group_list_bloc.dart b/lib/src/bloc/blocs/group_list/group_list_bloc.dart new file mode 100644 index 0000000..fdf1574 --- /dev/null +++ b/lib/src/bloc/blocs/group_list/group_list_bloc.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Business Logic Component for Group list +class GroupListBloc extends ElementListBloc { + final String _tag = '$GroupListBloc'; + + GroupListBloc({@required GroupRepository repository}) + : super(repository: repository); + + @override + GroupListState get initialState => GroupListUninitialized(); + + @override + Stream mapEventToState(GroupListEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is GroupListInitialized) { + yield* _mapGroupListInitializedEventToState(event); + } else if (event is GroupListRefresh) { + yield* _mapGroupListRefreshEventToState(event); + } else if (event is GroupListLoadMore) { + yield* _mapGroupListLoadMoreEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [GroupListInitialized] to [GroupListState] + /// + /// ```dart + /// yield* _mapGroupListInitializedEventToState(event); + /// ``` + Stream _mapGroupListInitializedEventToState( + GroupListInitialized event) async* { + print('$_tag:_mapGroupListInitializedEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + + parentId = event.parentId; + ownerId = event.ownerId; + cursor = event.cursor; + + elements = await _getGroups(cursor: cursor); + + yield GroupListLoaded(groups: elements); + } catch (error) { + yield GroupListFailure(error: error); + } + } + + /// Map [GroupListRefresh] to [GroupListState] + /// + /// ```dart + /// yield* _mapGroupListRefreshEventToState(event); + /// ``` + Stream _mapGroupListRefreshEventToState( + GroupListRefresh event) async* { + print('$_tag:_mapGroupListRefreshEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + elements = await _getGroups(cursor: cursor); + yield GroupListLoaded(groups: elements); + } catch (error) { + yield GroupListFailure(error: error); + } + } + + /// Map [GroupListLoadMore] to [GroupListState] + /// + /// ```dart + /// yield* _mapGroupListLoadMoreEventToState(event); + /// ``` + Stream _mapGroupListLoadMoreEventToState( + GroupListLoadMore event) async* { + print('$_tag:_mapGroupListLoadMoreEventToState($event)'); + try { + /// TODO: Add load more indicator stream + + final List groups = await _getGroups( + cursor: event.cursor.copyWith(offset: elements.length), + ); + + /// Append to elements + elements.addAll(groups); + + /// Save cursor limit if use list refreshed + cursor = cursor.copyWith(limit: elements.length); + + yield GroupListLoaded(groups: elements); + } catch (error) { + yield GroupListFailure(error: error); + } + } + + FutureOr> _getGroups({@required Cursor cursor}) async { + print('$_tag:_getGroups({cursor: $cursor})'); + if (parentId != null) { + return await repository.getGroupsFromPart( + parentId, + cursor: cursor, + ); + } else if (ownerId != null) { + return await repository.getGroupsFromUser( + ownerId, + cursor: cursor, + ); + } else { + return await repository.getList( + cursor: cursor, + ); + } + } +} diff --git a/lib/src/bloc/blocs/group_list/group_list_event.dart b/lib/src/bloc/blocs/group_list/group_list_event.dart new file mode 100644 index 0000000..bce5e24 --- /dev/null +++ b/lib/src/bloc/blocs/group_list/group_list_event.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [GroupListEvent] that must be dispatch to [GroupListBloc] +abstract class GroupListEvent extends Equatable { + GroupListEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class GroupListInitialized extends GroupListEvent + with ElementListInitialized { + GroupListInitialized({ + String parentPartId, + String ownerId, + Cursor cursor, + }) : assert( + parentPartId != null && ownerId == null, + '$GroupListInitialized must be created with a parentId or an ownerId', + ), + assert( + parentPartId == null && ownerId != null, + '$GroupListInitialized must be created with a parentId or an ownerId', + ), + super([parentPartId, ownerId]) { + this.parentId = parentPartId; + this.ownerId = ownerId; + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'parentPartId: $parentId, ' + 'ownerId: $ownerId, ' + 'cursor: $cursor' + ' }'; +} + +class GroupListRefresh extends GroupListEvent + with ElementListRefresh {} + +class GroupListLoadMore extends GroupListEvent + with ElementListLoadMore { + GroupListLoadMore({Cursor cursor}) + : assert(cursor != null), + super([cursor]) { + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'cursor: $cursor' + ' }'; +} diff --git a/lib/src/bloc/blocs/group_list/group_list_state.dart b/lib/src/bloc/blocs/group_list/group_list_state.dart new file mode 100644 index 0000000..a0b2875 --- /dev/null +++ b/lib/src/bloc/blocs/group_list/group_list_state.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class GroupListState extends Equatable { + GroupListState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class GroupListUninitialized extends GroupListState + with ElementListUninitialized {} + +class GroupListLoading extends GroupListState + with ElementListLoading { + GroupListLoading({int count = 0}) : super([count]) { + this.count = count; + } + + @override + String toString() => '$runtimeType{ ' + 'count: $count' + ' }'; +} + +class GroupListLoaded extends GroupListState + with ElementListLoaded { + GroupListLoaded({@required List groups}) : super([groups]) { + elements = groups; + } + + @override + String toString() => '$runtimeType{ ' + 'groups: $elements' + ' }'; +} + +class GroupListFailure extends GroupListState + with ElementListFailure { + GroupListFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/identity/identity.dart b/lib/src/bloc/blocs/identity/identity.dart new file mode 100644 index 0000000..a4fe72d --- /dev/null +++ b/lib/src/bloc/blocs/identity/identity.dart @@ -0,0 +1,3 @@ +export './identity_bloc.dart'; +export './identity_event.dart'; +export './identity_state.dart'; diff --git a/lib/src/bloc/blocs/identity/identity_bloc.dart b/lib/src/bloc/blocs/identity/identity_bloc.dart new file mode 100644 index 0000000..8245775 --- /dev/null +++ b/lib/src/bloc/blocs/identity/identity_bloc.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Account +class IdentityBloc extends Bloc { + final String _tag = '$IdentityBloc'; + + final IdentityRepository identityRepo; + final AuthenticationBloc authBloc; + StreamSubscription authBlocSubscription; + + IdentityBloc({ + @required this.identityRepo, + @required this.authBloc, + }) : assert(identityRepo != null, 'No $IdentityRepository given'), + super() { + authBlocSubscription = authBloc.state.listen((state) { + if (state is AuthenticationAuthenticated) { + dispatch(IdentityRefresh()); + } + }); + } + + @override + void dispose() { + authBlocSubscription.cancel(); + super.dispose(); + } + + @override + IdentityState get initialState => IdentityUninitialized(); + + @override + Stream mapEventToState(IdentityEvent event) async* { + print('$_tag:mapEventToState($event)'); + + if (event is IdentityRefresh) { + yield* _mapAccountRefreshToState(event); + } + } + + /// ----------------------------------------------------------------------- + /// All Event map to State + /// ----------------------------------------------------------------------- + + /// Map [IdentityRefresh] to [IdentityState] + /// + /// ```dart + /// yield* _mapAccountRefreshToState(event); + /// ``` + Stream _mapAccountRefreshToState( + IdentityRefresh event) async* { + try { + yield IdentityLoading(); + final userModel = await identityRepo.getIdentity(); + yield IdentityLoaded(user: userModel); + } catch (error) { + print('$_tag:_mapAccountRefreshToState -> ${error.runtimeType}'); + yield IdentityFailed(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/identity/identity_event.dart b/lib/src/bloc/blocs/identity/identity_event.dart new file mode 100644 index 0000000..c30d31f --- /dev/null +++ b/lib/src/bloc/blocs/identity/identity_event.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; + +/// [IdentityEvent] that must be dispatch to [AccountBloc] +abstract class IdentityEvent extends Equatable { + IdentityEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class IdentityRefresh extends IdentityEvent {} diff --git a/lib/src/bloc/blocs/identity/identity_state.dart b/lib/src/bloc/blocs/identity/identity_state.dart new file mode 100644 index 0000000..55ee837 --- /dev/null +++ b/lib/src/bloc/blocs/identity/identity_state.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class IdentityState extends Equatable { + IdentityState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class IdentityUninitialized extends IdentityState {} + +class IdentityLoading extends IdentityState {} + +class IdentityLoaded extends IdentityState { + final UserEntity user; + + IdentityLoaded({ + @required this.user, + }) : super([user]); + + @override + String toString() => '$runtimeType{ ' + 'userModel: $user' + ' }'; +} + +class IdentityFailed extends IdentityState { + final dynamic error; + + IdentityFailed({@required this.error}) + : assert(error != null, 'No error given'), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/login/login.dart b/lib/src/bloc/blocs/login/login.dart new file mode 100644 index 0000000..d3a5e51 --- /dev/null +++ b/lib/src/bloc/blocs/login/login.dart @@ -0,0 +1,3 @@ +export './login_bloc.dart'; +export './login_event.dart'; +export './login_state.dart'; diff --git a/lib/src/bloc/blocs/login/login_bloc.dart b/lib/src/bloc/blocs/login/login_bloc.dart new file mode 100644 index 0000000..d71f230 --- /dev/null +++ b/lib/src/bloc/blocs/login/login_bloc.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Login +class LoginBloc extends Bloc { + final String _tag = '$LoginBloc'; + + final CVAuthService cvAuthService; + + LoginBloc({ + @required this.cvAuthService, + }) : assert(cvAuthService != null, 'No $CVAuthService given'), + super(); + + @override + LoginState get initialState => LoginInitial(); + + @override + Stream mapEventToState(LoginEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is LoginButtonPressed) { + yield* _mapLoginButtonPressedToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [LoginButtonPressed] to [LoginState] + /// + /// ```dart + /// yield* _mapLoginButtonPressedToState(event); + /// ``` + Stream _mapLoginButtonPressedToState( + LoginButtonPressed event) async* { + try { + if (event is LoginButtonPressed) { + yield LoginLoading(); + + final auth = await cvAuthService.authenticate( + email: event.email, + password: event.password, + ); + + yield LoginSucceed( + accessToken: auth.accessToken, + accessTokenExpiration: auth.accessTokenExpiration, + refreshToken: auth.refreshToken, + ); + } + } catch (error) { + yield LoginFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/login/login_event.dart b/lib/src/bloc/blocs/login/login_event.dart new file mode 100644 index 0000000..636db38 --- /dev/null +++ b/lib/src/bloc/blocs/login/login_event.dart @@ -0,0 +1,26 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +/// [LoginEvent] that must be dispatch to [LoginBloc] +abstract class LoginEvent extends Equatable { + LoginEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class LoginButtonPressed extends LoginEvent { + final String email; + final String password; + + LoginButtonPressed({ + @required this.email, + @required this.password, + }) : super([email, password]); + + @override + String toString() => '$runtimeType{ ' + 'username: $email, ' + 'password: HIDDEN' + ' }'; +} diff --git a/lib/src/bloc/blocs/login/login_state.dart b/lib/src/bloc/blocs/login/login_state.dart new file mode 100644 index 0000000..52377a2 --- /dev/null +++ b/lib/src/bloc/blocs/login/login_state.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract class LoginState extends Equatable { + LoginState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class LoginInitial extends LoginState {} + +class LoginLoading extends LoginState {} + +class LoginFailure extends LoginState { + final dynamic error; + + LoginFailure({@required this.error}) + : assert(error != null, 'No error given'), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} + +class LoginSucceed extends LoginState { + final String accessToken; + final DateTime accessTokenExpiration; + final String refreshToken; + + LoginSucceed({ + @required this.accessToken, + @required this.accessTokenExpiration, + @required this.refreshToken, + }) : super([accessToken, accessTokenExpiration, refreshToken]); + + @override + String toString() => '$runtimeType{ ' + 'accessToken: $accessToken, ' + 'accessToken: $accessTokenExpiration, ' + 'refreshToken: $refreshToken ' + ' }'; +} diff --git a/lib/src/bloc/blocs/part/part.dart b/lib/src/bloc/blocs/part/part.dart new file mode 100644 index 0000000..ebfbda0 --- /dev/null +++ b/lib/src/bloc/blocs/part/part.dart @@ -0,0 +1,3 @@ +export 'part_bloc.dart'; +export 'part_event.dart'; +export 'part_state.dart'; diff --git a/lib/src/bloc/blocs/part/part_bloc.dart b/lib/src/bloc/blocs/part/part_bloc.dart new file mode 100644 index 0000000..0a3e08c --- /dev/null +++ b/lib/src/bloc/blocs/part/part_bloc.dart @@ -0,0 +1,80 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Part +class PartBloc + extends ElementBloc { + final String _tag = '$PartBloc'; + + PartBloc({@required PartRepository repository}) + : super(repository: repository); + + /// [_fallBackId] is used if [element] is never assigned and + /// an [PartRefresh] is dispatched + String _fallBackId; + + @override + PartState get initialState => PartUninitialized(); + + @override + Stream mapEventToState(PartEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is PartInitialized) { + yield* _mapInitializedEventToState(event); + } else if (event is PartRefresh) { + yield* _mapRefreshEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [PartInitialized] to [PartState] + /// + /// ```dart + /// yield* _mapInitializedEventToState(event); + /// ``` + Stream _mapInitializedEventToState(PartInitialized event) async* { + print('$_tag:_mapInitializedEventToState($event)'); + try { + yield PartLoading(); + + if (event.elementId != null) { + _fallBackId = event.elementId; + element = await repository.getById(event.elementId); + } else if (event.element != null) { + _fallBackId = event.element.id; + element = event.element; + } + + yield PartLoaded(part: element); + } catch (error) { + yield PartFailure(error: error); + } + } + + /// Map [PartRefresh] to [PartState] + /// + /// ```dart + /// yield* _mapRefreshEventToState(event); + /// ``` + Stream _mapRefreshEventToState(PartRefresh event) async* { + print('$_tag:_mapRefreshEventToState($event)'); + try { + yield PartLoading(); + + element = await repository.getById( + element?.id ?? _fallBackId, + force: true, + ); + + _fallBackId = element.id; + + yield PartLoaded(part: element); + } catch (error) { + yield PartFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/part/part_event.dart b/lib/src/bloc/blocs/part/part_event.dart new file mode 100644 index 0000000..1173417 --- /dev/null +++ b/lib/src/bloc/blocs/part/part_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class PartEvent extends Equatable { + PartEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class PartInitialized extends PartEvent with ElementInitialized { + PartInitialized({String partId, PartEntity part}) + : assert( + partId != null && part == null, + '$PartInitialized must be created with a $PartEntity or its ID', + ), + assert( + partId == null && part != null, + '$PartInitialized must be created with a $PartEntity or its ID', + ), + super([partId, part]) { + elementId = partId; + element = part; + } + + @override + String toString() => '$runtimeType{ ' + 'id: $elementId, ' + 'part: $element' + ' }'; +} + +class PartRefresh extends PartEvent with ElementRefresh {} diff --git a/lib/src/bloc/blocs/part/part_state.dart b/lib/src/bloc/blocs/part/part_state.dart new file mode 100644 index 0000000..d14f36c --- /dev/null +++ b/lib/src/bloc/blocs/part/part_state.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class PartState extends Equatable { + PartState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class PartUninitialized extends PartState + with ElementUninitialized {} + +class PartLoading extends PartState with ElementLoading {} + +class PartLoaded extends PartState with ElementLoaded { + PartLoaded({PartEntity part}) : super([part]) { + element = part; + } + + @override + String toString() { + return '$runtimeType{ part: $element }'; + } +} + +class PartFailure extends PartState with ElementFailure { + PartFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/part_list/part_list.dart b/lib/src/bloc/blocs/part_list/part_list.dart new file mode 100644 index 0000000..85aa6b1 --- /dev/null +++ b/lib/src/bloc/blocs/part_list/part_list.dart @@ -0,0 +1,3 @@ +export './part_list_bloc.dart'; +export './part_list_event.dart'; +export './part_list_state.dart'; diff --git a/lib/src/bloc/blocs/part_list/part_list_bloc.dart b/lib/src/bloc/blocs/part_list/part_list_bloc.dart new file mode 100644 index 0000000..7d2a5a8 --- /dev/null +++ b/lib/src/bloc/blocs/part_list/part_list_bloc.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Business Logic Component for Part list +class PartListBloc extends ElementListBloc { + final String _tag = '$PartListBloc'; + + PartListBloc({@required PartRepository repository}) + : super(repository: repository); + + @override + PartListState get initialState => PartListUninitialized(); + + @override + Stream mapEventToState(PartListEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is PartListInitialized) { + yield* _mapPartListInitializedEventToState(event); + } else if (event is PartListRefresh) { + yield* _mapPartListRefreshEventToState(event); + } else if (event is PartListLoadMore) { + yield* _mapPartListLoadMoreEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + Stream _mapPartListInitializedEventToState( + PartListInitialized event) async* { + print('$_tag:_mapPartListInitializedEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + + parentId = event.parentId; + ownerId = event.ownerId; + cursor = event.cursor; + + elements = await _getParts(cursor: cursor); + + yield PartListLoaded(parts: elements); + } catch (error) { + yield PartListFailure(error: error); + } + } + + Stream _mapPartListRefreshEventToState( + PartListRefresh event) async* { + print('$_tag:_mapPartListRefreshEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + elements = await _getParts(cursor: cursor); + yield PartListLoaded(parts: elements); + } catch (error) { + yield PartListFailure(error: error); + } + } + + /// Map [PartListLoadMore] to [PartListState] + /// + /// ```dart + /// yield* _mapPartListLoadMoreEventToState(event); + /// ``` + Stream _mapPartListLoadMoreEventToState( + PartListLoadMore event) async* { + print('$_tag:_mapPartListLoadMoreEventToState($event)'); + try { + /// TODO: Add load more indicator stream + + List parts = await _getParts( + cursor: event.cursor.copyWith(offset: elements.length), + ); + + /// Append to elements + elements.addAll(parts); + + /// Save cursor limit if use list refreshed + cursor = cursor.copyWith(limit: elements.length); + + yield PartListLoaded(parts: elements); + } catch (error) { + yield PartListFailure(error: error); + } + } + + FutureOr> _getParts({@required Cursor cursor}) async { + print('$_tag:_getParts({cursor: $cursor})'); + if (parentId != null) { + return await repository.getPartsFromProfile( + parentId, + cursor: cursor, + ); + } else if (ownerId != null) { + return await repository.getPartsFromUser( + ownerId, + cursor: cursor, + ); + } else { + return await repository.getList( + cursor: cursor, + ); + } + } +} diff --git a/lib/src/bloc/blocs/part_list/part_list_event.dart b/lib/src/bloc/blocs/part_list/part_list_event.dart new file mode 100644 index 0000000..8f69057 --- /dev/null +++ b/lib/src/bloc/blocs/part_list/part_list_event.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [PartListEvent] that must be dispatch to [PartListBloc] +abstract class PartListEvent extends Equatable { + PartListEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class PartListInitialized extends PartListEvent + with ElementListInitialized { + PartListInitialized({ + String parentProfileId, + String ownerId, + Cursor cursor, + }) : assert( + parentProfileId != null && ownerId == null, + '$PartListInitialized must be created with a parentId or an ownerId', + ), + assert( + parentProfileId == null && ownerId != null, + '$PartListInitialized must be created with a parentId or an ownerId', + ), + super([parentProfileId, ownerId]) { + this.parentId = parentProfileId; + this.ownerId = ownerId; + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'parentProfileId: $parentId, ' + 'ownerId: $ownerId, ' + 'cursor: $cursor' + ' }'; +} + +class PartListRefresh extends PartListEvent + with ElementListRefresh {} + +class PartListLoadMore extends PartListEvent + with ElementListLoadMore { + PartListLoadMore({Cursor cursor}) + : assert(cursor != null), + super([cursor]) { + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'cursor: $cursor' + ' }'; +} diff --git a/lib/src/bloc/blocs/part_list/part_list_state.dart b/lib/src/bloc/blocs/part_list/part_list_state.dart new file mode 100644 index 0000000..3ffd7a1 --- /dev/null +++ b/lib/src/bloc/blocs/part_list/part_list_state.dart @@ -0,0 +1,51 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class PartListState extends Equatable { + PartListState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class PartListUninitialized extends PartListState + with ElementListUninitialized {} + +class PartListLoading extends PartListState + with ElementListLoading { + PartListLoading({int count = 0}) : super([count]) { + this.count = count; + } + + @override + String toString() => '$runtimeType{ ' + 'count: $count' + ' }'; +} + +class PartListLoaded extends PartListState with ElementListLoaded { + PartListLoaded({@required List parts}) : super([parts]) { + elements = parts; + } + + @override + String toString() => '$runtimeType{ ' + 'parts: $elements' + ' }'; +} + +class PartListFailure extends PartListState + with ElementListFailure { + PartListFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/profile/profile.dart b/lib/src/bloc/blocs/profile/profile.dart new file mode 100644 index 0000000..a642f12 --- /dev/null +++ b/lib/src/bloc/blocs/profile/profile.dart @@ -0,0 +1,3 @@ +export 'profile_bloc.dart'; +export 'profile_event.dart'; +export 'profile_state.dart'; diff --git a/lib/src/bloc/blocs/profile/profile_bloc.dart b/lib/src/bloc/blocs/profile/profile_bloc.dart new file mode 100644 index 0000000..2306284 --- /dev/null +++ b/lib/src/bloc/blocs/profile/profile_bloc.dart @@ -0,0 +1,81 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for Profile +class ProfileBloc extends ElementBloc { + final String _tag = '$ProfileBloc'; + + ProfileBloc({@required ProfileRepository repository}) + : super(repository: repository); + + /// [_fallBackId] is used if [element] is never assigned and + /// an [ProfileRefresh] is dispatched + String _fallBackId; + + @override + ProfileState get initialState => ProfileUninitialized(); + + @override + Stream mapEventToState(ProfileEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is ProfileInitialized) { + yield* _mapInitializedEventToState(event); + } else if (event is ProfileRefresh) { + yield* _mapRefreshEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [ProfileInitialized] to [ProfileState] + /// + /// ```dart + /// yield* _mapInitializedEventToState(event); + /// ``` + Stream _mapInitializedEventToState( + ProfileInitialized event) async* { + print('$_tag:_mapInitializedEventToState($event)'); + try { + yield ProfileLoading(); + + if (event.elementId != null) { + _fallBackId = event.elementId; + element = await await repository.getById(event.elementId); + } else if (event.element != null) { + _fallBackId = event.element.id; + element = event.element; + } + + yield ProfileLoaded(profile: element); + } catch (error) { + yield ProfileFailure(error: error); + } + } + + /// Map [ProfileRefresh] to [ProfileState] + /// + /// ```dart + /// yield* _mapRefreshEventToState(event); + /// ``` + Stream _mapRefreshEventToState(ProfileRefresh event) async* { + print('$_tag:_mapRefreshEventToState($event)'); + try { + yield ProfileLoading(); + + element = await repository.getById( + element?.id ?? _fallBackId, + force: true, + ); + + _fallBackId = element.id; + + yield ProfileLoaded(profile: element); + } catch (error) { + yield ProfileFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/profile/profile_event.dart b/lib/src/bloc/blocs/profile/profile_event.dart new file mode 100644 index 0000000..0e58fb0 --- /dev/null +++ b/lib/src/bloc/blocs/profile/profile_event.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// [ProfileEvent] that must be dispatch to [ProfileBloc] + +abstract class ProfileEvent extends Equatable { + ProfileEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class ProfileInitialized extends ProfileEvent + with ElementInitialized { + ProfileInitialized({String profileId, ProfileEntity profile}) + : assert( + profileId != null && profile == null, + '$ProfileInitialized must be created with an $ProfileEntity or its ID', + ), + assert( + profileId == null && profile != null, + '$ProfileInitialized must be created with an $ProfileEntity or its ID', + ), + super([profileId, profile]) { + this.elementId = profileId; + this.element = profile; + } + + @override + String toString() => '$runtimeType{ ' + 'id: $elementId, ' + 'element: $element' + ' }'; +} + +class ProfileRefresh extends ProfileEvent with ElementRefresh {} diff --git a/lib/src/bloc/blocs/profile/profile_state.dart b/lib/src/bloc/blocs/profile/profile_state.dart new file mode 100644 index 0000000..8c6cecc --- /dev/null +++ b/lib/src/bloc/blocs/profile/profile_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class ProfileState extends Equatable { + ProfileState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class ProfileUninitialized extends ProfileState + with ElementUninitialized {} + +class ProfileLoading extends ProfileState with ElementLoading {} + +class ProfileLoaded extends ProfileState with ElementLoaded { + ProfileLoaded({ProfileEntity profile}) : super([profile]) { + element = profile; + } + + @override + String toString() { + return '$runtimeType{ ' + 'element: $element' + ' }'; + } +} + +class ProfileFailure extends ProfileState with ElementFailure { + ProfileFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/profile_list/profile_list.dart b/lib/src/bloc/blocs/profile_list/profile_list.dart new file mode 100644 index 0000000..0d7c3b8 --- /dev/null +++ b/lib/src/bloc/blocs/profile_list/profile_list.dart @@ -0,0 +1,3 @@ +export './profile_list_bloc.dart'; +export './profile_list_event.dart'; +export './profile_list_state.dart'; diff --git a/lib/src/bloc/blocs/profile_list/profile_list_bloc.dart b/lib/src/bloc/blocs/profile_list/profile_list_bloc.dart new file mode 100644 index 0000000..a9f0aff --- /dev/null +++ b/lib/src/bloc/blocs/profile_list/profile_list_bloc.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Business Logic Component for Profile list +class ProfileListBloc extends ElementListBloc { + final String _tag = '$ProfileListBloc'; + + ProfileListBloc({@required ProfileRepository repository}) + : super(repository: repository); + + @override + ProfileListState get initialState => ProfileListUninitialized(); + + @override + Stream mapEventToState(ProfileListEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is ProfileListInitialized) { + yield* _mapProfileListInitializedEventToState(event); + } else if (event is ProfileListRefresh) { + yield* _mapProfileListRefreshEventToState(event); + } else if (event is ProfileListLoadMore) { + yield* _mapProfileListLoadMoreEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [ProfileListInitialized] to [ProfileListState] + /// + /// ```dart + /// yield* _mapProfileListInitializedEventToState(event); + /// ``` + Stream _mapProfileListInitializedEventToState( + ProfileListInitialized event) async* { + print('$_tag:_mapProfileListInitializedEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + + parentId = event.parentId; + ownerId = event.ownerId; + cursor = event.cursor; + + elements = await _getProfiles(cursor: cursor); + + yield ProfileListLoaded(profiles: elements); + } catch (error) { + yield ProfileListFailure(error: error); + } + } + + /// Map [ProfileListRefresh] to [ProfileListState] + /// + /// ```dart + /// yield* _mapProfileListRefreshEventToState(event); + /// ``` + Stream _mapProfileListRefreshEventToState( + ProfileListRefresh event) async* { + print('$_tag:_mapProfileListRefreshEventToState($event)'); + try { + /// TODO: Add refresh indicator stream + elements = await _getProfiles(cursor: cursor); + yield ProfileListLoaded(profiles: elements); + } catch (error) { + yield ProfileListFailure(error: error); + } + } + + /// Map [ProfileListLoadMore] to [ProfileListState] + /// + /// ```dart + /// yield* _mapProfileListLoadMoreEventToState(event); + /// ``` + Stream _mapProfileListLoadMoreEventToState( + ProfileListLoadMore event) async* { + print('$_tag:_mapProfileListLoadMoreEventToState($event)'); + try { + /// TODO: Add load more indicator stream + + final List profiles = await _getProfiles( + cursor: event.cursor.copyWith(offset: elements.length), + ); + + /// Append to elements + elements.addAll(profiles); + + /// Save cursor limit if use list refreshed + cursor = cursor.copyWith(limit: elements.length); + + yield ProfileListLoaded(profiles: elements); + } catch (error) { + yield ProfileListFailure(error: error); + } + } + + FutureOr> _getProfiles({@required Cursor cursor}) async { + print('$_tag:_getProfiles({cursor: $cursor})'); + if (parentId != null) { + return await repository.getProfilesFromUser( + parentId, + cursor: cursor, + ); + } else if (ownerId != null) { + return await repository.getProfilesFromUser( + ownerId, + cursor: cursor, + ); + } else { + return await repository.getList( + cursor: cursor, + ); + } + } +} diff --git a/lib/src/bloc/blocs/profile_list/profile_list_event.dart b/lib/src/bloc/blocs/profile_list/profile_list_event.dart new file mode 100644 index 0000000..0ca2194 --- /dev/null +++ b/lib/src/bloc/blocs/profile_list/profile_list_event.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [ProfileListEvent] that must be dispatch to [ProfileListBloc] +abstract class ProfileListEvent extends Equatable { + ProfileListEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class ProfileListInitialized extends ProfileListEvent + with ElementListInitialized { + ProfileListInitialized({ + String parentUserId, + String ownerId, + Cursor cursor, + }) : assert( + parentUserId != null && ownerId == null, + '$ProfileListInitialized must be created with a parentId or an ownerId', + ), + assert( + parentUserId == null && ownerId != null, + '$ProfileListInitialized must be created with a parentId or an ownerId', + ), + super([parentUserId, ownerId]) { + this.parentId = parentUserId; + this.ownerId = ownerId; + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'parentUserId: $parentId, ' + 'ownerId: $ownerId, ' + 'cursor: $cursor' + ' }'; +} + +class ProfileListRefresh extends ProfileListEvent + with ElementListRefresh {} + +class ProfileListLoadMore extends ProfileListEvent + with ElementListLoadMore { + ProfileListLoadMore({Cursor cursor}) + : assert(cursor != null), + super([cursor]) { + this.cursor = cursor; + } + + @override + String toString() => '$runtimeType{ ' + 'cursor: $cursor' + ' }'; +} diff --git a/lib/src/bloc/blocs/profile_list/profile_list_state.dart b/lib/src/bloc/blocs/profile_list/profile_list_state.dart new file mode 100644 index 0000000..6e60d9a --- /dev/null +++ b/lib/src/bloc/blocs/profile_list/profile_list_state.dart @@ -0,0 +1,53 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class ProfileListState extends Equatable { + ProfileListState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class ProfileListUninitialized extends ProfileListState + with ElementListUninitialized {} + +class ProfileListLoading extends ProfileListState + with ElementListLoading { + ProfileListLoading({int count = 0}) : super([count]) { + this.count = count; + } + + @override + String toString() => '$runtimeType{ ' + 'count: $count' + ' }'; +} + +class ProfileListLoaded extends ProfileListState + with ElementListLoaded { + ProfileListLoaded({@required List profiles}) + : super([profiles]) { + elements = profiles; + } + + @override + String toString() => '$runtimeType{ ' + 'profiles: $elements' + ' }'; +} + +class ProfileListFailure extends ProfileListState + with ElementListFailure { + ProfileListFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/bloc/blocs/register/register.dart b/lib/src/bloc/blocs/register/register.dart new file mode 100644 index 0000000..991f214 --- /dev/null +++ b/lib/src/bloc/blocs/register/register.dart @@ -0,0 +1,3 @@ +export './register_bloc.dart'; +export './register_event.dart'; +export './register_state.dart'; diff --git a/lib/src/bloc/blocs/register/register_bloc.dart b/lib/src/bloc/blocs/register/register_bloc.dart new file mode 100644 index 0000000..9d4d56b --- /dev/null +++ b/lib/src/bloc/blocs/register/register_bloc.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +class RegisterBloc extends Bloc { + final String _tag = '$RegisterBloc'; + + final CVAuthService cvAuthService; + + RegisterBloc({@required this.cvAuthService}) + : assert(cvAuthService != null, 'No $CVAuthService given'), + super(); + + @override + RegisterState get initialState => RegisterInitial(); + + @override + Stream mapEventToState(RegisterEvent event) async* { + print('$_tag:mapEventToState($event)'); + + if (event is RegistrationEvent) { + yield* _mapRegistrationEventToState(event); + } + } + + /// ----------------------------------------------------------------------- + /// All Event map to State + /// ----------------------------------------------------------------------- + + /// Map [RegistrationEvent] to [RegisterState] + /// + /// ```dart + /// yield* _mapRegistrationEventToState(event); + /// ``` + Stream _mapRegistrationEventToState( + RegistrationEvent event) async* { + try { + if (event is RegistrationEvent) { + yield RegisterLoading(); + cvAuthService.register( + fName: event.fName, + lName: event.lName, + email: event.email, + password: event.password, + ); + await Future.delayed(Duration(seconds: 2)); + yield RegisterInitial(); + } + } catch (error) { + yield RegisterFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/register/register_event.dart b/lib/src/bloc/blocs/register/register_event.dart new file mode 100644 index 0000000..8b20138 --- /dev/null +++ b/lib/src/bloc/blocs/register/register_event.dart @@ -0,0 +1,29 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +/// [RegisterEvent] that must be dispatch to [RegisterBloc] +abstract class RegisterEvent extends Equatable { + RegisterEvent([List props = const []]) : super(props); +} + +class RegistrationEvent extends RegisterEvent { + final String fName; + final String lName; + final String email; + final String password; + + RegistrationEvent({ + @required this.fName, + @required this.lName, + @required this.email, + @required this.password, + }) : super([fName, lName, email, password]); + + @override + String toString() => '$runtimeType{ ' + 'fName: $fName, ' + 'lName: $lName, ' + 'email: $email, ' + 'password: HIDDEN' + ' }'; +} diff --git a/lib/src/bloc/blocs/register/register_state.dart b/lib/src/bloc/blocs/register/register_state.dart new file mode 100644 index 0000000..2ea9bc9 --- /dev/null +++ b/lib/src/bloc/blocs/register/register_state.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract class RegisterState extends Equatable { + RegisterState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class RegisterInitial extends RegisterState {} + +class RegisterLoading extends RegisterState {} + +class RegisterFailure extends RegisterState { + final dynamic error; + + RegisterFailure({@required this.error}) + : assert(error != null, 'No error given'), + super([error]); + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} + +class RegisterSucceed extends RegisterState { + final String accessToken; + final DateTime accessTokenExpiration; + final String refreshToken; + + RegisterSucceed({ + @required this.accessToken, + @required this.accessTokenExpiration, + @required this.refreshToken, + }) : super([accessToken, accessTokenExpiration, refreshToken]); + + @override + String toString() => '$runtimeType{ ' + 'accessToken: $accessToken, ' + 'accessToken: $accessTokenExpiration, ' + 'refreshToken: $refreshToken ' + ' }'; +} diff --git a/lib/src/bloc/blocs/user/user.dart b/lib/src/bloc/blocs/user/user.dart new file mode 100644 index 0000000..c0ab383 --- /dev/null +++ b/lib/src/bloc/blocs/user/user.dart @@ -0,0 +1,3 @@ +export 'user_bloc.dart'; +export 'user_event.dart'; +export 'user_state.dart'; diff --git a/lib/src/bloc/blocs/user/user_bloc.dart b/lib/src/bloc/blocs/user/user_bloc.dart new file mode 100644 index 0000000..0e01076 --- /dev/null +++ b/lib/src/bloc/blocs/user/user_bloc.dart @@ -0,0 +1,80 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Business Logic Component for User +class UserBloc + extends ElementBloc { + final String _tag = '$UserBloc'; + + UserBloc({@required UserRepository repository}) + : super(repository: repository); + + /// [_fallBackId] is used if [element] is never assigned and + /// an [UserRefresh] is dispatched + String _fallBackId; + + @override + UserState get initialState => UserUninitialized(); + + @override + Stream mapEventToState(UserEvent event) async* { + print('$_tag:mapEventToState($event)'); + if (event is UserInitialized) { + yield* _mapInitializedEventToState(event); + } else if (event is UserRefresh) { + yield* _mapRefreshEventToState(event); + } + } + + /// -------------------------------------------------------------------------- + /// All Event map to State + /// -------------------------------------------------------------------------- + + /// Map [UserInitialized] to [UserState] + /// + /// ```dart + /// yield* _mapInitializedEventToState(event); + /// ``` + Stream _mapInitializedEventToState(UserInitialized event) async* { + print('$_tag:_mapInitializedEventToState($event)'); + try { + yield UserLoading(); + + if (event.elementId != null) { + _fallBackId = event.elementId; + element = await await repository.getById(event.elementId); + } else if (event.element != null) { + _fallBackId = event.element.id; + element = event.element; + } + + yield UserLoaded(user: element); + } catch (error) { + yield UserFailure(error: error); + } + } + + /// Map [UserRefresh] to [UserState] + /// + /// ```dart + /// yield* _mapRefreshEventToState(event); + /// ``` + Stream _mapRefreshEventToState(UserRefresh event) async* { + print('$_tag:_mapRefreshEventToState($event)'); + try { + yield UserLoading(); + + element = await repository.getById( + element?.id ?? _fallBackId, + force: true, + ); + + _fallBackId = element.id; + + yield UserLoaded(user: element); + } catch (error) { + yield UserFailure(error: error); + } + } +} diff --git a/lib/src/bloc/blocs/user/user_event.dart b/lib/src/bloc/blocs/user/user_event.dart new file mode 100644 index 0000000..c5b04f2 --- /dev/null +++ b/lib/src/bloc/blocs/user/user_event.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// [UserEvent] that must be dispatch to [UserBloc] + +abstract class UserEvent extends Equatable { + UserEvent([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class UserInitialized extends UserEvent with ElementInitialized { + UserInitialized({String userId, UserEntity user}) + : assert(userId != null && user == null), + assert(userId == null && user != null), + super([userId, user]) { + this.elementId = userId; + this.element = user; + } + + @override + String toString() => '$runtimeType{ userId: $elementId, user: $element }'; +} + +class UserRefresh extends UserEvent with ElementRefresh {} diff --git a/lib/src/bloc/blocs/user/user_state.dart b/lib/src/bloc/blocs/user/user_state.dart new file mode 100644 index 0000000..fdfd6f8 --- /dev/null +++ b/lib/src/bloc/blocs/user/user_state.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class UserState extends Equatable { + UserState([List props = const []]) : super(props); + + @override + String toString() => '$runtimeType{}'; +} + +class UserUninitialized extends UserState + with ElementUninitialized {} + +class UserLoading extends UserState with ElementLoading {} + +class UserLoaded extends UserState with ElementLoaded { + UserLoaded({UserEntity user}) : super([user]) { + element = user; + } + + @override + String toString() { + return '$runtimeType{ ' + 'element: $element' + ' }'; + } +} + +class UserFailure extends UserState with ElementFailure { + UserFailure({@required dynamic error}) + : assert(error != null, 'No error given'), + super([error]) { + this.error = error; + } + + @override + String toString() => '$runtimeType{ ' + 'error: $error' + ' }'; +} diff --git a/lib/src/blocs/bloc_provider.dart b/lib/src/blocs/bloc_provider.dart deleted file mode 100644 index 13dfe0c..0000000 --- a/lib/src/blocs/bloc_provider.dart +++ /dev/null @@ -1,40 +0,0 @@ -///Generic Interface for all BLoCs -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; - -///Generic BLoC provider -class BlocProvider extends StatefulWidget { - BlocProvider({ - Key key, - @required this.child, - @required this.bloc, - }) : super(key: key); - - final T bloc; - final Widget child; - - @override - _BlocProviderState createState() => _BlocProviderState(); - - static T of(BuildContext context) { - final type = _typeOf>(); - BlocProvider provider = context.ancestorWidgetOfExactType(type); - - return provider.bloc; - } - - static Type _typeOf() => T; -} - -class _BlocProviderState extends State> { - @override - void dispose() { - widget.bloc.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} diff --git a/lib/src/blocs/main_bloc.dart b/lib/src/blocs/main_bloc.dart deleted file mode 100644 index d279c4d..0000000 --- a/lib/src/blocs/main_bloc.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:rxdart/rxdart.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; - -enum TabType { - HOME_TAB, - ACCOUNT_TAB, -} - -class MainBloc extends BlocBase { - MainBloc() : super() { - _tabController.add(TabType.HOME_TAB); - } - - ///Reactive variables - final _tabController = BehaviorSubject(); - - ///Streams - Observable get tabStream => _tabController.stream; - - ///Sinks - Sink get tab => _tabController.sink; - - /* Functions */ - - ///Human function - Function(TabType) get changeTab => tab.add; - - @override - void dispose() { - _tabController.close(); - } -} diff --git a/lib/src/commons/colors.dart b/lib/src/commons/colors.dart deleted file mode 100644 index e8f87eb..0000000 --- a/lib/src/commons/colors.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; - -class AppColors { - ///Colors - static const Color kCVBlue = Colors.blue; - static const Color kCVOrange = Colors.deepOrange; - static const Color kCVPink = Colors.pink; - static const Color kCVWhite = Colors.white; - static const Color kCVBlack = Colors.black; - - ///Basics - static const Color kCVPrimaryColor = const Color(0xFF2196f3); - static const Color kCVPrimaryColorLight = const Color(0xFF6ec6ff); - static const Color kCVPrimaryColorDark = const Color(0xFF0069c0); - static const Color kCVTextOnPrimary = const Color(0xFFFFFFFF); - static const Color kCVAccentColor = const Color(0xFFFF5722); - static const Color kCVAccentColorLight = const Color(0xFFff8a50); - static const Color kCVAccentColorDark = const Color(0xFFc41c00); - static const Color kCVTextOnAccent = const Color(0xFFFFFFFF); - - static const Color kCVBackgroundColor = const Color(0xFFFFFFFF); - static const Color kCVBackgroundColorLight = kCVBackgroundColor; - static const Color kCVBackgroundColorDark = const Color(0xFF353A3A); - - ///Cards - static const Color kCVCardBackgroundColor = const Color(0xFFFFFFFF); - static const Color kCVCardBackgroundColorLight = kCVBackgroundColor; - static const Color kCVCardBackgroundColorDark = const Color(0xFF353A3A); - - /// Misc - static const Color kCVErrorRed = Colors.red; - - /// Auth Stuff - static const Color loginGradientEnd = kCVPrimaryColorLight; - static const Color loginGradientStart = kCVPrimaryColorDark; -} diff --git a/lib/src/commons/dimensions.dart b/lib/src/commons/dimensions.dart deleted file mode 100644 index c5a1786..0000000 --- a/lib/src/commons/dimensions.dart +++ /dev/null @@ -1,27 +0,0 @@ -class AppDimensions { - ///Group - static const double kCVGroupPadding = 5.0; - - ///Entry - static const double kCVEntryPadding = 10.0; - static const double kCVEntryTagSpacing = 4.0; - static const double kCVEntryCardElevation = 2.0; - static const double kCVEntryEventHeight = 200.0; - static const double kCVEntryEventHWidth = 300.0; - - static const double kCVHorizontalEntryListHeight = kCVEntryEventHeight; - static const double kCVHorizontalGroupListHeight = 300.0; - - ///Sort Dialog - static const double kCVSortDialogWidth = 200.0; - static const double kCVSortDialogHeight = 300.0; - - ///List - static const double kCVListHeaderDefaultHeightMax = 40.0; - static const double kCVListHeaderDefaultHeightMin = 40.0; - - ///Profile - static const double kCVProfileAvatarMin = 5.0; - static const double kCVProfileAvatarMax = 50.0; - static const double kCVProfileAvatarElevation = 2.0; -} diff --git a/lib/src/commons/tags.dart b/lib/src/commons/tags.dart deleted file mode 100644 index 4beb430..0000000 --- a/lib/src/commons/tags.dart +++ /dev/null @@ -1 +0,0 @@ -const String kHeroSearchFAB = 'TAG_HERO_SEARCH_FAB'; diff --git a/lib/src/data/cache_model.dart b/lib/src/data/cache_model.dart new file mode 100644 index 0000000..9762a8d --- /dev/null +++ b/lib/src/data/cache_model.dart @@ -0,0 +1,16 @@ +import 'package:meta/meta.dart'; + +class CacheModel { + CacheModel({ + @required this.model, + @required this.expiration, + }) : assert(model != null), + assert(model != null); + + T model; + DateTime expiration; + + bool isExpired() { + return DateTime.now().compareTo(expiration) >= 0 ? true : false; + } +} diff --git a/lib/src/data/exceptions/api_exceptions.dart b/lib/src/data/exceptions/api_exceptions.dart new file mode 100644 index 0000000..621a815 --- /dev/null +++ b/lib/src/data/exceptions/api_exceptions.dart @@ -0,0 +1,59 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +class ApiException extends AppException { + ApiException._internal({ + @required AppExceptionType type, + String message, + StackTrace stackTrace, + }) : assert(type != null, ' No $AppExceptionType given'), + super(type: type, message: message, stackTrace: stackTrace); + + factory ApiException.fromDioRequest({ + @required String errorCode, + String message, + StackTrace stackTrace, + }) { + assert(errorCode != null); + + final AppExceptionType errorType = _apiErrorCodes[errorCode]; + + return ApiException._internal( + type: errorType ?? AppExceptionType.somethingWentWrong, + message: message, + stackTrace: stackTrace, + ); + } +} + +/// Error map between api errors and domain exception types +Map _apiErrorCodes = { + /// -------------------------------------------------------------------------- + /// Server + /// -------------------------------------------------------------------------- + + 'SERVER_ERROR': AppExceptionType.serverSideProblem, + + /// -------------------------------------------------------------------------- + /// Authentication + /// -------------------------------------------------------------------------- + + 'AUTH_LOGIN_FAILED': AppExceptionType.authLoginFailed, + 'AUTH_REGISTRATION_FAILED': AppExceptionType.authRegistrationFailed, + 'AUTH_ACCOUNT_ALREADY_EXISTS': AppExceptionType.authAccountAlreadyExists, + 'AUTH_ACCOUNT_DISABLED': AppExceptionType.authAccountDisabled, + 'AUTH_NOT_AUTHORIZED': AppExceptionType.authUnauthorized, + 'AUTH_FORBIDDEN': AppExceptionType.authForbidden, + + /// -------------------------------------------------------------------------- + /// User + /// -------------------------------------------------------------------------- + + 'USER_NOT_FOUND': AppExceptionType.userNotFound, + + /// -------------------------------------------------------------------------- + /// Form + /// -------------------------------------------------------------------------- + + 'FORM_PASSWORD_WRONG_POLICY': AppExceptionType.formPasswordWrongPolicy, +}; diff --git a/lib/src/data/managers/api_interceptor.dart b/lib/src/data/managers/api_interceptor.dart new file mode 100644 index 0000000..2bfe157 --- /dev/null +++ b/lib/src/data/managers/api_interceptor.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart' as domain; + +class ApiInterceptor extends Interceptor { + String _clientId; + String _clientSecret; + String _accessToken; + String _refreshToken; + + InterceptorsWrapper _interceptorWrapper; + + ApiInterceptor({ + String clientId, + String clientSecret, + String accessToken, + String refreshToken, + }) { + _clientId = clientId; + _clientSecret = clientSecret; + _accessToken = accessToken; + _refreshToken = refreshToken; + + _interceptorWrapper = + InterceptorsWrapper(onRequest: _onRequest, onResponse: _onResponse); + } + + InterceptorsWrapper get interceptorsWrapper => _interceptorWrapper; + + RequestOptions _onRequest(RequestOptions options) { + // Adding client credentials + if (_clientId != null && _clientSecret != null) { + options.headers.addAll({ + 'client_id': '$_clientId', + 'client_secret': '$_clientSecret', + 'grant_type': 'password', + }); + } + + // Adding access token + if (_accessToken != null) { + options.headers + .addAll({HttpHeaders.authorizationHeader: 'Bearer $_accessToken'}); + } + + // Adding refresh token + if (_refreshToken != null) { + options.headers.addAll({'refresh_token': '$_refreshToken'}); + } + + return options; + } + + Response _onResponse(Response response) { + /// Save access token + final data = response.data; + if (data is Map) { + if (data.containsKey('access_token')) { + _accessToken = data['access_token'] as String; + } + + /// Save refresh token + if (data.containsKey('refresh_token')) { + _refreshToken = response.data['refresh_token'] as String; + } + } + return response; + } + + FutureOr deleteAuthData() async { + _accessToken = null; + } + + @override + String toString() => '$runtimeType{}'; +} + +/// Parse response to check if there is any error +dynamic checkApiResponse(Response response, {StackTrace stackTrace}) { + // TODO: use Envelop type for Response body + final Map body = response.data as Map; + final String apiErrorCode = body['error'] as String; + final String apiMessage = body['message'] as String; + + if (apiErrorCode != null) { + throw ApiException.fromDioRequest( + errorCode: apiErrorCode, + message: apiMessage, + stackTrace: stackTrace ?? StackTrace.current, + ); + } + return response; +} + +dynamic apiErrorCatcher(dynamic err) { + if (err is DioError && err.response != null) { + final Response response = err.response; + final StackTrace stackTrace = (err?.error as dynamic).stackTrace as StackTrace; + + checkApiResponse(response, stackTrace: stackTrace); + + final statusCode = err?.response?.statusCode; + final statusMessage = err?.response?.statusMessage; + + if (statusCode != null) { + throw domain.HttpException( + statusCode: statusCode, + statusMessage: statusMessage, + stackTrace: stackTrace ?? StackTrace.current, + ); + } + } + return err; +} diff --git a/lib/src/data/managers/app_shared_preferences_manager.dart b/lib/src/data/managers/app_shared_preferences_manager.dart new file mode 100644 index 0000000..f36ff7e --- /dev/null +++ b/lib/src/data/managers/app_shared_preferences_manager.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +/// Application preferences manager implementation +/// providing [AppPrefsDataStore] +class AppPrefsManager implements AppPrefsDataStore { + final String _keyAppDarkMode = 'APP_DARK_MODE'; + + FutureOr get _prefs => SharedPreferences.getInstance(); + + AppPrefsManager(); + + /// -------------------------------------------------------------------------- + /// Dark Mode + /// -------------------------------------------------------------------------- + + @override + FutureOr getDarkMode() async { + final storage = await _prefs; + return storage.getBool(_keyAppDarkMode); + } + + @override + FutureOr toggleDarkMode(bool darkMode) async { + final storage = await _prefs; + return await storage.setBool( + _keyAppDarkMode, + darkMode, + ); + } + + @override + FutureOr deleteDarkMode() async { + final storage = await _prefs; + await storage.remove(_keyAppDarkMode); + return null; + } + + /// -------------------------------------------------------------------------- + /// All + /// -------------------------------------------------------------------------- + + @override + Future deleteAll() async { + final storage = await _prefs; + await storage.remove(_keyAppDarkMode); + } +} diff --git a/lib/src/data/managers/auth_shared_preferences_manager.dart b/lib/src/data/managers/auth_shared_preferences_manager.dart new file mode 100644 index 0000000..b0d134f --- /dev/null +++ b/lib/src/data/managers/auth_shared_preferences_manager.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +/// One of the possible Implementation of PreferencesService Interface +class AuthSharedPreferencesManager implements AuthInfoDataStore { + final String _keyOAuthAccessToken = 'OAUTH_ACCESS_TOKEN'; + final String _keyOAuthAccessTokenExpiration = 'OAUTH_ACCESS_TOKEN_EXPIRATION'; + final String _keyOAuthRefreshToken = 'OAUTH_REFRESH_TOKEN'; + final String _keyOAuthRefreshTokenExpiration = + 'OAUTH_REFRESH_TOKEN_EXPIRATION'; + + FutureOr get _prefs => SharedPreferences.getInstance(); + + AuthSharedPreferencesManager(); + + /// ---------------------------------------------------------- + /// ------------------------- Tokens ------------------------- + /// ---------------------------------------------------------- + + @override + FutureOr getAccessToken() async { + final prefs = await _prefs; + return prefs.getString(_keyOAuthAccessToken); + } + + @override + FutureOr setAccessToken(String token) async { + final prefs = await _prefs; + return (await prefs.setString(_keyOAuthAccessToken, token)) ? token : null; + } + + @override + FutureOr deleteAccessToken() async { + final prefs = await _prefs; + await prefs.remove(_keyOAuthAccessToken); + return null; + } + + @override + FutureOr getAccessTokenExpiration() async { + final prefs = await _prefs; + return DateTime.parse(prefs.getString(_keyOAuthAccessTokenExpiration)); + } + + @override + FutureOr setAccessTokenExpiration(DateTime expiration) async { + final prefs = await _prefs; + return (await prefs.setString( + _keyOAuthAccessTokenExpiration, expiration.toIso8601String())) + ? expiration + : null; + } + + @override + FutureOr deleteAccessTokenExpiration() async { + final prefs = await _prefs; + await prefs.remove(_keyOAuthAccessTokenExpiration); + return null; + } + + @override + FutureOr getRefreshToken() async { + final prefs = await _prefs; + return prefs.getString(_keyOAuthRefreshToken); + } + + @override + FutureOr setRefreshToken(String refreshToken) async { + final prefs = await _prefs; + return (await prefs.setString(_keyOAuthRefreshToken, refreshToken)) + ? refreshToken + : null; + } + + @override + FutureOr deleteRefreshToken() async { + final prefs = await _prefs; + await prefs.remove(_keyOAuthRefreshToken); + return null; + } + + @override + FutureOr getRefreshTokenExpiration() async { + final prefs = await _prefs; + return DateTime.parse(prefs.getString(_keyOAuthRefreshTokenExpiration)); + } + + @override + FutureOr setRefreshTokenExpiration(DateTime expiration) async { + final prefs = await _prefs; + return (await prefs.setString( + _keyOAuthRefreshTokenExpiration, expiration.toIso8601String())) + ? expiration + : null; + } + + @override + FutureOr deleteRefreshTokenExpiration() async { + final prefs = await _prefs; + await prefs.remove(_keyOAuthRefreshTokenExpiration); + return null; + } + + /// ---------------------------------------------------------- + /// -------------------------- All --------------------------- + /// ---------------------------------------------------------- + + Future deleteAll() async { + await deleteAccessToken(); + await deleteAccessTokenExpiration(); + await deleteRefreshToken(); + await deleteRefreshTokenExpiration(); + } +} diff --git a/lib/src/data/managers/config_assets_manager.dart b/lib/src/data/managers/config_assets_manager.dart new file mode 100644 index 0000000..e7f384f --- /dev/null +++ b/lib/src/data/managers/config_assets_manager.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/data/models/config_model.dart'; + +/// From https://medium.com/@sokrato/storing-your-secret-keys-in-flutter-c0b9af1c0f69 +class ConfigAssetsManager implements FoundationConfigService { + static const _configPath = 'config.json'; + ConfigDataModel _config; + + ConfigAssetsManager(); + + FutureOr _load() async { + final jsonMap = await rootBundle.loadStructuredData>( + _configPath, + (jsonStr) { + return Future.value(json.decode(jsonStr) as Map); + }, + ); + _config = ConfigDataModel.fromJson(jsonMap); + } + + @override + FutureOr getApiServerUrl() async { + if (_config == null) await _load(); + return Future.value(_config.apiServerUrl); + } + + @override + FutureOr getClientId() async { + if (_config == null) await _load(); + return Future.value(_config.clientId); + } + + @override + FutureOr getClientSecret() async { + if (_config == null) await _load(); + return Future.value(_config.clientSecret); + } +} diff --git a/lib/src/data/managers/cv_api_manager.dart b/lib/src/data/managers/cv_api_manager.dart new file mode 100644 index 0000000..8902431 --- /dev/null +++ b/lib/src/data/managers/cv_api_manager.dart @@ -0,0 +1,572 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Default Implementation of [CVApiManager] +class CVApiManager + implements + CVAuthService, + IdentityDataStore, + UserDataStore, + ProfileDataStore, + PartDataStore, + GroupDataStore, + EntryDataStore { + final String _tag = '$CVApiManager'; + + final String apiBaseUrl; + + Dio _dio; + + static const String _pathOauth = '/oauth'; + static const String _pathOauthToken = '$_pathOauth/token'; + + static const String _pathMe = '/me'; + static const String _pathUsers = '/users'; + static const String _pathProfiles = '/profiles'; + static const String _pathParts = '/parts'; + static const String _pathGroups = '/groups'; + static const String _pathEntries = '/entries'; + + final ApiInterceptor tokenInterceptor; + + CVApiManager({ + @required this.apiBaseUrl, + @required this.tokenInterceptor, + }) : assert(apiBaseUrl != null, 'Missing api base url'), + assert(tokenInterceptor != null, 'No $ApiInterceptor given') { + _dio = Dio(BaseOptions( + baseUrl: apiBaseUrl, + connectTimeout: 3000, + contentType: '${ContentType.json}', + responseType: ResponseType.json, + )); + + // Add Interceptor + _dio.interceptors.add(tokenInterceptor.interceptorsWrapper); + + _dio.interceptors.add(InterceptorsWrapper( + onResponse: checkApiResponse, + onError: apiErrorCatcher, + )); + } + + /// -------------------------------------------------------------------------- + /// CVAuthService + /// -------------------------------------------------------------------------- + + @override + FutureOr authenticate({ + @required String email, + @required String password, + }) async { + print('$_tag:authenticate'); + + try { + final RequestAuthDataModel oauthModel = RequestAuthDataModel( + username: email, + password: password, + ); + + final Response> response = + await _dio.post>(_pathOauthToken, + data: oauthModel.toJson()); + + final model = ResponseAuthDataModel.fromJson(response.data); + return model; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr register({ + String fName, + String lName, + String email, + String password, + }) { + // TODO: implement register + throw NotImplementedYetError(); + } + + /// Logout + @override + FutureOr logout() async { + await tokenInterceptor.deleteAuthData(); + } + + /// -------------------------------------------------------------------------- + /// IdentityDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getIdentity() async { + print('$_tag:getIdentity()'); + + try { + final Response> response = + await _dio.get>(_pathMe); + + final DataEnvelop envelop = + DataEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setIdentity(UserDataModel userModel) { + // TODO: implement setIdentity + throw NotImplementedYetError(); + } + + FutureOr> getProfilesFromIdentity({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getProfilesFromIdentity'); + + try { + const String _path = '$_pathMe$_pathProfiles'; + + final Response> response = + await _dio.get>( + _path, + queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }, + ); + + final envelop = + DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// UserDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getUser(String userId) async { + print('$_tag:getUser($userId)'); + + try { + final String _path = '$_pathUsers/$userId'; + + final Response> response = + await _dio.get>(_path); + final envelop = DataEnvelop.fromJson(response.data); + + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setUser(UserDataModel userModel) { + print('$_tag:setUser($userModel)'); + // TODO: implement setUser + throw NotImplementedYetError(); + } + + @override + FutureOr> getUsers({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getUsers'); + + try { + const String _path = _pathUsers; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// ProfileDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getProfile(String profileId) async { + print('$_tag:fetchProfile($profileId)'); + + try { + final String _path = '$_pathProfiles/$profileId'; + + final Response> response = + await _dio.get>(_path); + final envelop = DataEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setProfile(ProfileDataModel profileModel) { + print('$_tag:setProfile($profileModel)'); + // TODO: implement setProfile + throw NotImplementedYetError(); + } + + @override + FutureOr> getProfiles({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getProfiles'); + + try { + const String _path = _pathProfiles; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = + DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getProfilesFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:fetchProfilesFromUser'); + + try { + final String _path = '$_pathUsers/$userId$_pathProfiles'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = + DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// PartDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getPart(String partId) async { + print('$_tag:fetchPart($partId)'); + + try { + final String _path = '$_pathParts/$partId'; + + final Response> response = + await _dio.get>(_path); + final envelop = DataEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setPart(PartDataModel partModel) { + // TODO: implement setPart + throw NotImplementedYetError(); + } + + @override + FutureOr> getParts({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getParts'); + + try { + const String _path = _pathParts; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getPartsFromProfile( + String profileId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:fetchPartsFromProfile'); + + try { + final String _path = '$_pathProfiles/$profileId$_pathParts'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + 'sort': '+order' + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getPartsFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:fetchPartsFromUser($userId)'); + + try { + final String _path = '$_pathUsers/$userId$_pathParts'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// GroupDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getGroup(String groupId) async { + print('$_tag:getGroup($groupId)'); + + try { + final String _path = '$_pathGroups/$groupId'; + + final Response> response = + await _dio.get>(_path); + final envelop = DataEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setGroup(GroupDataModel groupModel) { + print('$_tag:setGroup($groupModel)'); +// TODO: implement setGroup + throw NotImplementedYetError(); + } + + @override + FutureOr> getGroups({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getGroups'); + + try { + const String _path = _pathGroups; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getGroupsFromPart( + String partId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getGroupsFromPart($partId)'); + + try { + final String _path = '$_pathParts/$partId$_pathGroups'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + 'sort': '+order', + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getGroupsFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getGroupsFromUser($userId)'); + + try { + final String _path = '$_pathUsers/$userId$_pathGroups'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// EntryDataStore + /// -------------------------------------------------------------------------- + + @override + FutureOr getEntry(String entryId) async { + print('$_tag:getEntry($entryId)'); + + try { + final String _path = '$_pathEntries/$entryId'; + + final Response> response = + await _dio.get>(_path); + final envelop = DataEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getEntries({ + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getEntries'); + + try { + const String _path = _pathEntries; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr setEntry(EntryDataModel entryModel) { + print('$_tag:setEntry($entryModel)'); + // TODO: implement setEntry + throw NotImplementedYetError(); + } + + @override + FutureOr> getEntriesFromGroup( + String groupId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getEntriesFromGroup($groupId)'); + + try { + final String _path = '$_pathGroups/$groupId$_pathEntries'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + 'sort': '+order', + }); + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + @override + FutureOr> getEntriesFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getEntriesFromUser($userId)'); + + try { + final String _path = '$_pathUsers/$userId$_pathEntries'; + + final Response> response = + await _dio.get>(_path, queryParameters: { + 'offset': cursor.offset.toString(), + 'limit': cursor.limit.toString(), + }); + + final envelop = DataArrayEnvelop.fromJson(response.data); + return envelop.data; + } on DioError catch (e) { + throw e.error ?? e; + } + } + + /// -------------------------------------------------------------------------- + /// Misc + /// -------------------------------------------------------------------------- + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/models/base_model.dart b/lib/src/data/models/base_model.dart new file mode 100644 index 0000000..19d8ec6 --- /dev/null +++ b/lib/src/data/models/base_model.dart @@ -0,0 +1,28 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class BaseDataModel implements BaseEntity { + @JsonKey(name: '_id') + @override + String id; + + @JsonKey(name: 'createdAt') + @override + DateTime createdAt; + + @JsonKey(name: 'updatedAt') + @override + DateTime updatedAt; + + @JsonKey(name: '__v') + @override + int version; + + BaseDataModel({ + @required this.id, + this.createdAt, + this.updatedAt, + this.version, + }) : super(); +} diff --git a/lib/src/data/models/config_model.dart b/lib/src/data/models/config_model.dart new file mode 100644 index 0000000..394931f --- /dev/null +++ b/lib/src/data/models/config_model.dart @@ -0,0 +1,34 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'config_model.g.dart'; + +@JsonSerializable() +class ConfigDataModel { + @JsonKey(name: 'apiServerUrl') + String apiServerUrl; + + @JsonKey(name: 'clientId') + String clientId; + + @JsonKey(name: 'clientSecret') + String clientSecret; + + ConfigDataModel({ + @required this.apiServerUrl, + @required this.clientId, + @required this.clientSecret, + }) : super(); + + factory ConfigDataModel.fromJson(Map json) => + _$ConfigDataModelFromJson(json); + + Map toJson() => _$ConfigDataModelToJson(this); + + @override + String toString() => '$runtimeType{ ' + 'apiServerUrl: $apiServerUrl, ' + 'clientId: $clientId, ' + 'clientSecret: $clientSecret' + ' }'; +} diff --git a/lib/src/data/models/element_model.dart b/lib/src/data/models/element_model.dart new file mode 100644 index 0000000..0d53677 --- /dev/null +++ b/lib/src/data/models/element_model.dart @@ -0,0 +1,17 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/data/models/base_model.dart'; + +abstract class ElementDataModel extends BaseDataModel implements ElementEntity { + ElementDataModel({ + @required String id, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); +} diff --git a/lib/src/data/models/entry_model.dart b/lib/src/data/models/entry_model.dart new file mode 100644 index 0000000..d947b0a --- /dev/null +++ b/lib/src/data/models/entry_model.dart @@ -0,0 +1,61 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'entry_model.g.dart'; + +@JsonSerializable() +class EntryDataModel extends ElementDataModel implements EntryEntity { + @JsonKey(name: 'name') + @override + String name; + + @JsonKey(name: 'type') + @override + String type; + + @JsonKey(name: 'content') + @override + dynamic content; + + @JsonKey(name: 'startDate') + @override + String startDate; + + @JsonKey(name: 'endDate') + @override + String endDate; + + @JsonKey(name: 'location') + @override + String location; + + @JsonKey(name: 'owner') + @override + String ownerId; + + EntryDataModel({ + @required String id, + this.name, + this.type, + this.content, + this.startDate, + this.endDate, + this.location, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + factory EntryDataModel.fromJson(Map json) => + _$EntryDataModelFromJson(json); + + Map toJson() => _$EntryDataModelToJson(this); +} diff --git a/lib/src/data/models/envelop_models.dart b/lib/src/data/models/envelop_models.dart new file mode 100644 index 0000000..903d82f --- /dev/null +++ b/lib/src/data/models/envelop_models.dart @@ -0,0 +1,176 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'envelop_models.g.dart'; + +/// ---------------------------------------------------------------------------- +/// Envelop +/// ---------------------------------------------------------------------------- + +abstract class _Envelop extends Object { + @JsonKey(name: 'error') + final bool error; + + @JsonKey(name: 'message') + final String message; + + _Envelop({ + this.error, + this.message, + }); +} + +/// Envelop containing [data] of type [T] +@JsonSerializable() +class DataEnvelop extends _Envelop { + @_GenericConverter() + T data; + + DataEnvelop({ + bool error, + String message, + this.data, + }) : super(error: error, message: message); + + factory DataEnvelop.fromJson(Map json) => + _$DataEnvelopFromJson(json); + + Map toJson() => _$DataEnvelopToJson(this); +} + +/// Envelop with containing [data] list of type [T] +@JsonSerializable() +class DataArrayEnvelop extends _Envelop { + @_GenericConverter() + List data; + + int total; + + DataArrayEnvelop({ + bool error, + String message, + this.data, + this.total, + }) : super(error: error, message: message); + + factory DataArrayEnvelop.fromJson(Map json) => + _$DataArrayEnvelopFromJson(json); + + Map toJson() => _$DataArrayEnvelopToJson(this); +} + +/// Provide json serialization and deserialization methods +/// for generic/template field type +/// +/// Working for generic type only +/// +/// Based on +/// https://github.com/dart-lang/json_serializable/blob/ee2c5c788279af01860624303abe16811850b82c/example/lib/json_converter_example.dart +/// +/// Example: +/// ```dart +/// @_GenericConverter +/// List generics +/// +/// @_GenericConverter +/// T generic +/// ``` +/// +class _GenericConverter implements JsonConverter { + const _GenericConverter(); + + @override + T fromJson(Object json) { + print('fromJson'); + final T t = (T as dynamic)?.fromJson(json) as T + // This will only work if `json` is a native JSON type: + // num, String, bool, null, etc + // *and* is assignable to `T`. + ?? + json as T; + + if (t == null) { + throw Exception('Type $T no supported'); + } + return t; + } + + @override + Object toJson(T object) { + print('toJson'); + // This will only work if `object` is a native JSON type: + // num, String, bool, null, etc + // Or if it has a `toJson()` function`. + return (object as dynamic)?.toJson() ?? object; + } +} + +@JsonSerializable() +class RequestAuthDataModel extends Object { + @JsonKey(name: 'username') + final String username; + @JsonKey(name: 'password') + final String password; + + RequestAuthDataModel({ + @required this.username, + @required this.password, + }) : assert(username != null && password != null), + super(); + + factory RequestAuthDataModel.fromJson(Map json) => + _$RequestAuthDataModelFromJson(json); + + Map toJson() => _$RequestAuthDataModelToJson(this); + + @override + String toString() => '$runtimeType{ ' + 'username: $username, ' + 'password: HIDDEN' + ' }'; +} + +@JsonSerializable() +class ResponseAuthDataModel implements AuthEntity { + @JsonKey(name: 'access_token') + @override + String accessToken; + + @JsonKey(name: 'refresh_token') + @override + String refreshToken; + + @JsonKey(name: 'expires_in') + int accessTokenExpiresIn; + + @JsonKey(name: 'token_type') + @override + String tokenType; + + @override + DateTime accessTokenExpiration; + + ResponseAuthDataModel({ + this.accessToken, + this.refreshToken, + this.accessTokenExpiresIn, + this.tokenType, + }) : super() { + accessTokenExpiration = + DateTime.now().add(Duration(milliseconds: accessTokenExpiresIn)); + } + + factory ResponseAuthDataModel.fromJson(Map json) => + _$ResponseAuthDataModelFromJson(json); + + Map toJson() => _$ResponseAuthDataModelToJson(this); + + @override + String toString() => '$runtimeType{ ' + 'accessToken: $accessToken, ' + 'refreshToken: $refreshToken, ' + 'accessTokenExpiresAt: $accessTokenExpiresIn, ' + 'tokenType: $tokenType' + ' }'; +} diff --git a/lib/src/data/models/group_model.dart b/lib/src/data/models/group_model.dart new file mode 100644 index 0000000..f0e19bc --- /dev/null +++ b/lib/src/data/models/group_model.dart @@ -0,0 +1,46 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'group_model.g.dart'; + +@JsonSerializable() +class GroupDataModel extends ElementDataModel implements GroupEntity { + @JsonKey(name: 'name') + @override + String name; + + @JsonKey(name: 'entries') + @override + List entryIds; + + @JsonKey(name: 'type') + @override + String type; + + @JsonKey(name: 'owner') + @override + String ownerId; + + GroupDataModel({ + @required String id, + this.name, + this.type, + this.entryIds, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + factory GroupDataModel.fromJson(Map json) => + _$GroupDataModelFromJson(json); + + Map toJson() => _$GroupDataModelToJson(this); +} diff --git a/lib/src/data/models/part_model.dart b/lib/src/data/models/part_model.dart new file mode 100644 index 0000000..4d27deb --- /dev/null +++ b/lib/src/data/models/part_model.dart @@ -0,0 +1,46 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'part_model.g.dart'; + +@JsonSerializable() +class PartDataModel extends ElementDataModel implements PartEntity { + @JsonKey(name: 'name') + @override + String name; + + @JsonKey(name: 'groups') + @override + List groupIds; + + @JsonKey(name: 'type') + @override + String type; + + @JsonKey(name: 'owner') + @override + String ownerId; + + PartDataModel({ + @required String id, + this.name, + this.groupIds, + this.type, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + factory PartDataModel.fromJson(Map json) => + _$PartDataModelFromJson(json); + + Map toJson() => _$PartDataModelToJson(this); +} diff --git a/lib/src/data/models/profile_model.dart b/lib/src/data/models/profile_model.dart new file mode 100644 index 0000000..80a4dde --- /dev/null +++ b/lib/src/data/models/profile_model.dart @@ -0,0 +1,61 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'profile_model.g.dart'; + +@JsonSerializable() +class ProfileDataModel extends ElementDataModel implements ProfileEntity { + @JsonKey(name: 'title') + @override + String title; + + @JsonKey(name: 'subtitle') + @override + String subtitle; + + @JsonKey(name: 'picture') + @override + Uri picture; + + @JsonKey(name: 'cover') + @override + Uri cover; + + @JsonKey(name: 'parts') + @override + List partIds; + + @JsonKey(name: 'type') + @override + String type; + + @JsonKey(name: 'owner') + @override + String ownerId; + + ProfileDataModel({ + @required String id, + this.title, + this.subtitle, + this.picture, + this.cover, + this.partIds, + this.type, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + factory ProfileDataModel.fromJson(Map json) => + _$ProfileDataModelFromJson(json); + + Map toJson() => _$ProfileDataModelToJson(this); +} diff --git a/lib/src/data/models/user_model.dart b/lib/src/data/models/user_model.dart new file mode 100644 index 0000000..c4b2821 --- /dev/null +++ b/lib/src/data/models/user_model.dart @@ -0,0 +1,67 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class UserDataModel extends ElementDataModel implements UserEntity { + @JsonKey(name: 'disabled') + @override + bool disabled; + + @JsonKey(name: 'email') + @override + String email; + + @JsonKey(name: 'username') + @override + String username; + + @JsonKey(name: 'picture') + @override + String picture; + + @JsonKey(name: 'profiles') + @override + List profileIds; + + @JsonKey(name: 'permission') + @override + dynamic permission; + + UserDataModel({ + @required String id, + this.disabled, + this.email, + this.username, + this.picture, + this.profileIds, + this.permission, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + factory UserDataModel.fromJson(Map json) => + _$UserDataModelFromJson(json); + + Map toJson() => _$UserDataModelToJson(this); + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'disabled: $disabled, ' + 'email: $email, ' + 'username: $username, ' + 'picture: $picture, ' + 'profileIds: $profileIds, ' + 'permission: $permission' + ' }'; +} diff --git a/lib/src/data/repositories/app_prefs_repository.dart b/lib/src/data/repositories/app_prefs_repository.dart new file mode 100644 index 0000000..c4cfb72 --- /dev/null +++ b/lib/src/data/repositories/app_prefs_repository.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +class ImplAppPrefsRepository extends AppPrefsRepository { + final AppPrefsDataStoreFactory factory; + + ImplAppPrefsRepository({@required this.factory}) + : assert(factory != null, 'No $AppPrefsDataStoreFactory given'); + + @override + FutureOr toggleDarkMode(bool darkMode) { + return factory.create.toggleDarkMode(darkMode); + } + + @override + FutureOr getDarkMode() { + return factory.create.getDarkMode(); + } + + @override + FutureOr deleteDarkMode() { + return factory.create.getDarkMode(); + } + + @override + FutureOr deleteAll() { + return factory.create.deleteAll(); + } +} diff --git a/lib/src/data/repositories/auth_info_repository.dart b/lib/src/data/repositories/auth_info_repository.dart new file mode 100644 index 0000000..0f687a2 --- /dev/null +++ b/lib/src/data/repositories/auth_info_repository.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +class ImplAuthInfoRepository implements AuthInfoRepository { + final AuthInfoDataStoreFactory factory; + + ImplAuthInfoRepository({@required this.factory}) + : assert(factory != null, 'No $AuthInfoDataStoreFactory given'); + + /// -------------------------------------------------------------------------- + /// Access Token + /// -------------------------------------------------------------------------- + + @override + FutureOr getAccessToken() { + return factory.diskDataStore.getAccessToken(); + } + + @override + FutureOr setAccessToken(String token) { + return factory.diskDataStore.setAccessToken(token); + } + + @override + FutureOr deleteAccessToken() { + return factory.diskDataStore.deleteAccessToken(); + } + + @override + FutureOr getAccessTokenExpiration() { + return factory.diskDataStore.getAccessTokenExpiration(); + } + + @override + FutureOr setAccessTokenExpiration(DateTime expiration) { + return factory.diskDataStore.setAccessTokenExpiration(expiration); + } + + @override + FutureOr deleteAccessTokenExpiration() { + return factory.diskDataStore.deleteAccessTokenExpiration(); + } + + /// -------------------------------------------------------------------------- + /// Refresh Token + /// -------------------------------------------------------------------------- + + @override + FutureOr getRefreshToken() { + return factory.diskDataStore.getRefreshToken(); + } + + @override + FutureOr setRefreshToken(String token) { + return factory.diskDataStore.setRefreshToken(token); + } + + @override + FutureOr deleteRefreshToken() { + return factory.diskDataStore.deleteRefreshToken(); + } + + @override + FutureOr getRefreshTokenExpiration() { + return factory.diskDataStore.getRefreshTokenExpiration(); + } + + @override + FutureOr setRefreshTokenExpiration(DateTime expiration) { + return factory.diskDataStore.setRefreshTokenExpiration(expiration); + } + + @override + FutureOr deleteRefreshTokenExpiration() { + return factory.diskDataStore.deleteRefreshTokenExpiration(); + } +} diff --git a/lib/src/data/repositories/entry_repository.dart b/lib/src/data/repositories/entry_repository.dart new file mode 100644 index 0000000..bd07941 --- /dev/null +++ b/lib/src/data/repositories/entry_repository.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ImplEntryRepository extends EntryRepository { + final String _tag = '$ImplEntryRepository'; + + final EntryDataStoreFactory factory; + + ImplEntryRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getById( + String id, { + bool force = false, + }) async { + print('$_tag:getById($id)'); + assert(id != null); + + EntryDataModel dataModel; + + if (!force) dataModel = await factory.memoryDataStore.getEntry(id); + + if (dataModel == null) { + dataModel = await factory.cloudDataStore.getEntry(id); + factory.memoryDataStore.setEntry(dataModel); + } + + return dataModel; + } + + @override + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getList'); + + final dataModels = await factory.cloudDataStore.getEntries( + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setEntry); + + return dataModels; + } + + @override + FutureOr> getEntriesFromGroup( + String groupId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getEntriesFromGroup($groupId)'); + assert(groupId != null); + + final dataModels = await factory.cloudDataStore.getEntriesFromGroup( + groupId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setEntry); + + return dataModels; + } + + @override + FutureOr> getEntriesFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getEntriesFromUser($userId)'); + assert(userId != null); + + final dataModels = await factory.cloudDataStore.getEntriesFromUser( + userId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setEntry); + + return dataModels; + } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/repositories/group_repository.dart b/lib/src/data/repositories/group_repository.dart new file mode 100644 index 0000000..78e6336 --- /dev/null +++ b/lib/src/data/repositories/group_repository.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ImplGroupRepository extends GroupRepository { + final String _tag = '$ImplGroupRepository'; + + final GroupDataStoreFactory factory; + + ImplGroupRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getById( + String id, { + bool force = false, + }) async { + print('$_tag:getById($id)'); + assert(id != null); + + GroupDataModel dataModel; + + if (!force) dataModel = await factory.memoryDataStore.getGroup(id); + + if (dataModel == null) { + dataModel = await factory.cloudDataStore.getGroup(id); + await factory.memoryDataStore.setGroup(dataModel); + } + + return dataModel; + } + + @override + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getList'); + + final dataModels = await factory.cloudDataStore.getGroups( + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setGroup); + + return dataModels; + } + + @override + FutureOr> getGroupsFromPart( + String partId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + assert(partId != null); + print('$_tag:getGroupsFromPart($partId)'); + + final dataModels = await factory.cloudDataStore.getGroupsFromPart( + partId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setGroup); + + return dataModels; + } + + @override + FutureOr> getGroupsFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + assert(userId != null); + print('$_tag:getGroupsFromUser($userId)'); + + final dataModels = await factory.cloudDataStore.getGroupsFromUser( + userId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setGroup); + + return dataModels; + } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/repositories/identity_repository.dart b/lib/src/data/repositories/identity_repository.dart new file mode 100644 index 0000000..44461f9 --- /dev/null +++ b/lib/src/data/repositories/identity_repository.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +class ImplIdentityRepository extends IdentityRepository { + final String _tag = '$ImplIdentityRepository'; + + final IdentityDataStoreFactory factory; + + ImplIdentityRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getIdentity() async { + print('$_tag:getAccount'); + + final dataModel = await factory.memoryDataStore.getIdentity(); + + if (dataModel == null) { + final dataModel = await factory.cloudDataStore.getIdentity(); + factory.memoryDataStore.setIdentity(dataModel); + } + + return dataModel; + } + +// @override +// FutureOr> getProfilesFromIdentity({ +// /// TODO: Add filters +// /// TODO: Add sort +// Cursor cursor = const Cursor(), +// }) async { +// print('$_tag:getProfilesFromIdentity'); +// +// final dataModels = await factory.cloudDataStore.getProfilesFromIdentity( +// cursor: cursor, +// ); +// +// return dataModels; +// } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/repositories/part_repository.dart b/lib/src/data/repositories/part_repository.dart new file mode 100644 index 0000000..2b88946 --- /dev/null +++ b/lib/src/data/repositories/part_repository.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ImplPartRepository extends PartRepository { + final String _tag = '$ImplPartRepository'; + + final PartDataStoreFactory factory; + + ImplPartRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getById( + String id, { + bool force = false, + }) async { + print('$_tag:getById($id)'); + + PartDataModel dataModel; + + if (!force) dataModel = await factory.memoryDataStore.getPart(id); + + if (dataModel == null) { + dataModel = await factory.cloudDataStore.getPart(id); + factory.memoryDataStore.setPart(dataModel); + } + + return dataModel; + } + + @override + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getList'); + + final dataModels = await factory.cloudDataStore.getParts( + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setPart); + + return dataModels; + } + + @override + FutureOr> getPartsFromProfile( + String profileId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + assert(profileId != null); + print('$_tag:getPartsFromProfile'); + + final dataModels = await factory.cloudDataStore.getPartsFromProfile( + profileId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setPart); + + return dataModels; + } + + @override + FutureOr> getPartsFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getPartsFromUser'); + + final dataModels = await factory.cloudDataStore.getPartsFromUser( + userId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setPart); + + return dataModels; + } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/repositories/profile_repository.dart b/lib/src/data/repositories/profile_repository.dart new file mode 100644 index 0000000..b08ce2b --- /dev/null +++ b/lib/src/data/repositories/profile_repository.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ImplProfileRepository extends ProfileRepository { + final String _tag = '$ImplProfileRepository'; + + final ProfileDataStoreFactory factory; + + ImplProfileRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getById( + String id, { + bool force = false, + }) async { + print('$_tag:getById($id)'); + assert(id != null); + + ProfileDataModel dataModel; + + if (!force) dataModel = await factory.memoryDataStore.getProfile(id); + + if (dataModel == null) { + dataModel = await factory.cloudDataStore.getProfile(id); + factory.memoryDataStore.setProfile(dataModel); + } + + return dataModel; + } + + @override + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getList'); + + final dataModels = await factory.cloudDataStore.getProfiles( + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setProfile); + + return dataModels; + } + + @override + FutureOr> getProfilesFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getProfilesFromUser($userId)'); + + final dataModels = await factory.cloudDataStore.getProfilesFromUser( + userId, + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setProfile); + + return dataModels; + } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/repositories/user_repository.dart b/lib/src/data/repositories/user_repository.dart new file mode 100644 index 0000000..6152b45 --- /dev/null +++ b/lib/src/data/repositories/user_repository.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ImplUserRepository extends UserRepository { + final String _tag = '$ImplUserRepository'; + + final UserDataStoreFactory factory; + + ImplUserRepository({@required this.factory}) : assert(factory != null); + + @override + FutureOr getById( + String id, { + bool force = false, + }) async { + assert(id != null); + print('$_tag:getUser($id)'); + + UserDataModel dataModel; + + if (!force) dataModel = await factory.memoryDataStore.getUser(id); + + if (dataModel == null) { + dataModel = await factory.cloudDataStore.getUser(id); + factory.memoryDataStore.setUser(dataModel); + } + + return dataModel; + } + + @override + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }) async { + print('$_tag:getUsers'); + + final dataModels = await factory.cloudDataStore.getUsers( + cursor: cursor, + ); + + // Store in RAM + dataModels.map(factory.memoryDataStore.setUser); + + return dataModels; + } + + @override + String toString() => '$runtimeType{ ' + 'factory: $factory' + ' }'; +} diff --git a/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store.dart b/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store.dart new file mode 100644 index 0000000..8263aa8 --- /dev/null +++ b/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store.dart @@ -0,0 +1,17 @@ +import 'dart:async'; + +abstract class AppPrefsDataStore { + /// Get App dark mode + /// + /// Must return the application dark mode [bool] or [null] if not found + FutureOr getDarkMode(); + + /// Set Application dark mode([bool]) + FutureOr toggleDarkMode(bool darkMode); + + /// Delete Application dark mode + FutureOr deleteDarkMode(); + + /// Delete all application preferences + FutureOr deleteAll(); +} diff --git a/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store_factory.dart b/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store_factory.dart new file mode 100644 index 0000000..468f5ad --- /dev/null +++ b/lib/src/data/stores/app_prefs_data_store/app_prefs_data_store_factory.dart @@ -0,0 +1,18 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class AppPrefsDataStoreFactory { + final String _tag = '$AppPrefsDataStoreFactory'; + + final AppPrefsDataStore diskDataStore; + + AppPrefsDataStore get create => diskDataStore; + + AppPrefsDataStoreFactory({@required this.diskDataStore}) + : assert(diskDataStore != null, 'No disk $AppPrefsDataStore given'); + + @override + String toString() => '$runtimeType{ ' + 'diskDataStore: $diskDataStore' + ' }'; +} diff --git a/lib/src/data/stores/auth_info_data_store/auth_info_data_store.dart b/lib/src/data/stores/auth_info_data_store/auth_info_data_store.dart new file mode 100644 index 0000000..b9f4203 --- /dev/null +++ b/lib/src/data/stores/auth_info_data_store/auth_info_data_store.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +abstract class AuthInfoDataStore { + /// -------------------------------------------------------------------------- + /// Access Token + /// -------------------------------------------------------------------------- + + /// Set access token ([String]) + FutureOr setAccessToken(String accessToken); + + /// Get access token + /// + /// Must return a access token ([String]) or [null] otherwise + FutureOr getAccessToken(); + + /// Delete access token + FutureOr deleteAccessToken(); + + /// Set access token expiration datetime as([DateTime]) + FutureOr setAccessTokenExpiration(DateTime expiration); + + /// Get access token expiration time + /// + /// Must return the access token expiration datetime ([DateTime]) + /// or [null] otherwise + FutureOr getAccessTokenExpiration(); + + /// Delete access token expiration datetime + FutureOr deleteAccessTokenExpiration(); + + /// -------------------------------------------------------------------------- + /// Refresh Token + /// -------------------------------------------------------------------------- + + /// Set refresh token ([String]) + FutureOr setRefreshToken(String token); + + /// Get refresh token + /// + /// Must return a refresh token ([String]) or [null] otherwise + FutureOr getRefreshToken(); + + /// Delete refresh token + FutureOr deleteRefreshToken(); + + /// Set refresh token expiration datetime as timestamp ([int]) + FutureOr setRefreshTokenExpiration(DateTime expiration); + + /// Get refresh token expiration datetime + /// + /// Must return the access token expiration datetime ([DateTime]) + /// or [null] otherwise + FutureOr getRefreshTokenExpiration(); + + /// Delete refresh token expiration date + FutureOr deleteRefreshTokenExpiration(); + + /// -------------------------------------------------------------------------- + /// Misc + /// -------------------------------------------------------------------------- + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/auth_info_data_store/auth_info_data_store_factory.dart b/lib/src/data/stores/auth_info_data_store/auth_info_data_store_factory.dart new file mode 100644 index 0000000..9811b2d --- /dev/null +++ b/lib/src/data/stores/auth_info_data_store/auth_info_data_store_factory.dart @@ -0,0 +1,18 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class AuthInfoDataStoreFactory { + final String _tag = '$AuthInfoDataStoreFactory'; + + final AuthInfoDataStore diskDataStore; + + AuthInfoDataStore get create => diskDataStore; + + AuthInfoDataStoreFactory({@required this.diskDataStore}) + : assert(diskDataStore != null, 'No disk $AuthInfoDataStore given'); + + @override + String toString() => '$runtimeType{ ' + 'diskDataStore: $diskDataStore' + ' }'; +} diff --git a/lib/src/data/stores/entry_data_store/entry_data_store.dart b/lib/src/data/stores/entry_data_store/entry_data_store.dart new file mode 100644 index 0000000..1e18fab --- /dev/null +++ b/lib/src/data/stores/entry_data_store/entry_data_store.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +abstract class EntryDataStore { + FutureOr getEntry(String entryId); + + FutureOr setEntry(EntryDataModel entryModel); + + FutureOr> getEntries({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getEntriesFromGroup( + String groupId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getEntriesFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/data/stores/entry_data_store/entry_data_store_factory.dart b/lib/src/data/stores/entry_data_store/entry_data_store_factory.dart new file mode 100644 index 0000000..eaef772 --- /dev/null +++ b/lib/src/data/stores/entry_data_store/entry_data_store_factory.dart @@ -0,0 +1,23 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class EntryDataStoreFactory { + final String _tag = '$EntryDataStoreFactory'; + + final EntryDataStore cloudDataStore; + final EntryDataStore memoryDataStore; + + EntryDataStore get create => cloudDataStore; + + EntryDataStoreFactory({ + @required this.cloudDataStore, + @required this.memoryDataStore, + }) : assert(cloudDataStore != null, 'No cloud $EntryDataStore given'), + assert(memoryDataStore != null, 'No memory $EntryDataStore given'); + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/stores/entry_data_store/memory_entry_data_store.dart b/lib/src/data/stores/entry_data_store/memory_entry_data_store.dart new file mode 100644 index 0000000..490eddc --- /dev/null +++ b/lib/src/data/stores/entry_data_store/memory_entry_data_store.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Memory implementation of [EntryDataStore] +class MemoryEntryDataStore implements EntryDataStore { + final String _tag = '$MemoryEntryDataStore'; + + MemoryEntryDataStore(); + + final _entries = >{}; + + @override + FutureOr getEntry(String entryId) async { + print('$_tag:getEntry($entryId)'); + + final CacheModel cacheModel = _entries[entryId]; + return (cacheModel != null && !cacheModel.isExpired()) + ? cacheModel.model + : null; + } + + @override + FutureOr setEntry(EntryDataModel entryModel) async { + print('$_tag:setEntry($entryModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + final cacheModel = + CacheModel(model: entryModel, expiration: expiration); + _entries[entryModel.id] = cacheModel; + + return cacheModel.model; + } + + @override + FutureOr> getEntries({ + Cursor cursor = const Cursor(), + }) { + return _entries.values.map((value) => value.model).toList(); + } + + @override + FutureOr> getEntriesFromGroup( + String groupId, { + Cursor cursor = const Cursor(), + }) { + // TODO: implement getEntriesFromGroup + throw NotImplementedYetError(); + } + + @override + FutureOr> getEntriesFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) { + return _entries.values + .map((e) => e.model) + .where((model) => model.ownerId == userId) + .toList(); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/group_date_store/group_data_store.dart b/lib/src/data/stores/group_date_store/group_data_store.dart new file mode 100644 index 0000000..d16537e --- /dev/null +++ b/lib/src/data/stores/group_date_store/group_data_store.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +abstract class GroupDataStore { + FutureOr getGroup(String groupId); + + FutureOr setGroup(GroupDataModel groupModel); + + FutureOr> getGroups({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getGroupsFromPart( + String partId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getGroupsFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/data/stores/group_date_store/group_data_store_factory.dart b/lib/src/data/stores/group_date_store/group_data_store_factory.dart new file mode 100644 index 0000000..c2dc15a --- /dev/null +++ b/lib/src/data/stores/group_date_store/group_data_store_factory.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class GroupDataStoreFactory { + final GroupDataStore cloudDataStore; + final GroupDataStore memoryDataStore; + + GroupDataStoreFactory({ + @required this.cloudDataStore, + @required this.memoryDataStore, + }) : assert(cloudDataStore != null, 'No cloud $GroupDataStore given'), + assert(memoryDataStore != null, 'No memory $GroupDataStore given'); + + GroupDataStore get create => cloudDataStore; + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/stores/group_date_store/memory_group_data_store.dart b/lib/src/data/stores/group_date_store/memory_group_data_store.dart new file mode 100644 index 0000000..ec9af3e --- /dev/null +++ b/lib/src/data/stores/group_date_store/memory_group_data_store.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Memory implementation of [GroupDataStore] +class MemoryGroupDataStore implements GroupDataStore { + final String _tag = '$MemoryGroupDataStore'; + + MemoryGroupDataStore(); + + final _groups = >{}; + + @override + FutureOr getGroup(String groupId) async { + print('$_tag:getGroup($groupId)'); + + final CacheModel cacheModel = _groups[groupId]; + return (cacheModel != null && !cacheModel.isExpired()) + ? cacheModel.model + : null; + } + + @override + FutureOr setGroup(GroupDataModel groupModel) async { + print('$_tag:setGroup($groupModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + final cacheModel = + CacheModel(model: groupModel, expiration: expiration); + _groups[groupModel.id] = cacheModel; + + return cacheModel.model; + } + + @override + FutureOr> getGroups({ + Cursor cursor = const Cursor(), + }) { + return _groups.values.map((value) => value.model).toList(); + } + + @override + FutureOr> getGroupsFromPart( + String partId, { + Cursor cursor = const Cursor(), + }) { + // TODO: implement getGroupsFromPart + return null; + } + + @override + FutureOr> getGroupsFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) { + return _groups.values + .map((e) => e.model) + .where((model) => model.ownerId == userId) + .toList(); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/identity_data_store/identity_data_store.dart b/lib/src/data/stores/identity_data_store/identity_data_store.dart new file mode 100644 index 0000000..a0953f4 --- /dev/null +++ b/lib/src/data/stores/identity_data_store/identity_data_store.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; + +abstract class IdentityDataStore { + FutureOr getIdentity(); + + FutureOr setIdentity(UserDataModel userModel); +} diff --git a/lib/src/data/stores/identity_data_store/identity_data_store_factory.dart b/lib/src/data/stores/identity_data_store/identity_data_store_factory.dart new file mode 100644 index 0000000..adb9430 --- /dev/null +++ b/lib/src/data/stores/identity_data_store/identity_data_store_factory.dart @@ -0,0 +1,22 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class IdentityDataStoreFactory { + final String _tag = '$IdentityDataStoreFactory'; + + final IdentityDataStore memoryDataStore; + final IdentityDataStore cloudDataStore; + + IdentityDataStoreFactory( + {@required this.memoryDataStore, @required this.cloudDataStore}) + : assert(memoryDataStore != null, ' No memory $IdentityDataStore given'), + assert(cloudDataStore != null, ' No cloud $IdentityDataStore given'); + + IdentityDataStore get create => memoryDataStore; + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/stores/identity_data_store/memory_identity_data_store.dart b/lib/src/data/stores/identity_data_store/memory_identity_data_store.dart new file mode 100644 index 0000000..450fd88 --- /dev/null +++ b/lib/src/data/stores/identity_data_store/memory_identity_data_store.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; + +/// Memory implementation of [IdentityDataStore] +class MemoryIdentityDataStore implements IdentityDataStore { + final String _tag = '$MemoryIdentityDataStore'; + + MemoryIdentityDataStore(); + + CacheModel _accountCache; + + @override + FutureOr getIdentity() async { + print('$_tag:getAccount()'); + + return (_accountCache != null && !_accountCache.isExpired()) + ? _accountCache.model + : null; + } + + @override + FutureOr setIdentity(UserDataModel userModel) async { + print('$_tag:setAccount($userModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + _accountCache = + CacheModel(model: userModel, expiration: expiration); + + return _accountCache.model; + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/part_date_store/memory_part_data_store.dart b/lib/src/data/stores/part_date_store/memory_part_data_store.dart new file mode 100644 index 0000000..4c42451 --- /dev/null +++ b/lib/src/data/stores/part_date_store/memory_part_data_store.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Memory implementation of [PartDataStore] +class MemoryPartDataStore implements PartDataStore { + final String _tag = '$MemoryPartDataStore'; + + MemoryPartDataStore(); + + final _parts = >{}; + + @override + FutureOr getPart(String partId) async { + print('$_tag:getPart($partId)'); + + final CacheModel cacheModel = _parts[partId]; + return (cacheModel != null && !cacheModel.isExpired()) + ? cacheModel.model + : null; + } + + @override + FutureOr setPart(PartDataModel partModel) async { + print('$_tag:setPart($partModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + final cacheModel = + CacheModel(model: partModel, expiration: expiration); + _parts[partModel.id] = cacheModel; + + return cacheModel.model; + } + + @override + FutureOr> getParts({ + Cursor cursor = const Cursor(), + }) { + return _parts.values.map((value) => value.model).toList(); + } + + @override + FutureOr> getPartsFromProfile( + String profileId, { + Cursor cursor = const Cursor(), + }) { + // TODO: implement getPartsFromProfile + throw NotImplementedYetError(); + } + + @override + FutureOr> getPartsFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) { + return _parts.values + .map((e) => e.model) + .where((model) => model.ownerId == userId) + .toList(); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/part_date_store/part_data_store.dart b/lib/src/data/stores/part_date_store/part_data_store.dart new file mode 100644 index 0000000..bb372c6 --- /dev/null +++ b/lib/src/data/stores/part_date_store/part_data_store.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +abstract class PartDataStore { + FutureOr getPart(String partId); + + FutureOr setPart(PartDataModel partModel); + + FutureOr> getParts({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getPartsFromProfile( + String profileId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getPartsFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/data/stores/part_date_store/part_data_store_factory.dart b/lib/src/data/stores/part_date_store/part_data_store_factory.dart new file mode 100644 index 0000000..5e844c9 --- /dev/null +++ b/lib/src/data/stores/part_date_store/part_data_store_factory.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class PartDataStoreFactory { + final PartDataStore cloudDataStore; + final PartDataStore memoryDataStore; + + PartDataStoreFactory({ + @required this.cloudDataStore, + @required this.memoryDataStore, + }) : assert(cloudDataStore != null, 'No cloud $PartDataStore given'), + assert(memoryDataStore != null, 'No memory $PartDataStore given'); + + PartDataStore get create => cloudDataStore; + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/stores/profile_date_store/memory_profile_data_store.dart b/lib/src/data/stores/profile_date_store/memory_profile_data_store.dart new file mode 100644 index 0000000..7919786 --- /dev/null +++ b/lib/src/data/stores/profile_date_store/memory_profile_data_store.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Memory implementation of [ProfileDataStore] +class MemoryProfileDataStore implements ProfileDataStore { + final String _tag = '$MemoryProfileDataStore'; + + MemoryProfileDataStore(); + + final _profiles = >{}; + + @override + FutureOr getProfile(String profileId) async { + print('$_tag:getProfile($profileId)'); + + final CacheModel cacheModel = _profiles[profileId]; + return (cacheModel != null && !cacheModel.isExpired()) + ? cacheModel.model + : null; + } + + @override + FutureOr setProfile(ProfileDataModel profileModel) async { + print('$_tag:setProfile($profileModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + final cacheModel = CacheModel( + model: profileModel, expiration: expiration); + _profiles[profileModel.id] = cacheModel; + + return cacheModel.model; + } + + @override + FutureOr> getProfiles({ + Cursor cursor = const Cursor(), + }) { + return _profiles.values.map((value) => value.model).toList(); + } + + @override + FutureOr> getProfilesFromUser( + String userId, { + Cursor cursor = const Cursor(), + }) { + return _profiles.values + .map((value) => value.model) + .where((model) => model.ownerId == userId) + .toList(); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/profile_date_store/profile_data_store.dart b/lib/src/data/stores/profile_date_store/profile_data_store.dart new file mode 100644 index 0000000..87476ef --- /dev/null +++ b/lib/src/data/stores/profile_date_store/profile_data_store.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +abstract class ProfileDataStore { + FutureOr getProfile(String profileId); + + FutureOr setProfile(ProfileDataModel profileModel); + + FutureOr> getProfiles({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + FutureOr> getProfilesFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/data/stores/profile_date_store/profile_data_store_factory.dart b/lib/src/data/stores/profile_date_store/profile_data_store_factory.dart new file mode 100644 index 0000000..bba21db --- /dev/null +++ b/lib/src/data/stores/profile_date_store/profile_data_store_factory.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class ProfileDataStoreFactory { + final ProfileDataStore cloudDataStore; + final ProfileDataStore memoryDataStore; + + ProfileDataStoreFactory({ + @required this.cloudDataStore, + @required this.memoryDataStore, + }) : assert(cloudDataStore != null, 'No cloud $ProfileDataStore given'), + assert(memoryDataStore != null, 'No memory $ProfileDataStore given'); + + ProfileDataStore get create => cloudDataStore; + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/stores/user_data_store/memory_user_data_store.dart b/lib/src/data/stores/user_data_store/memory_user_data_store.dart new file mode 100644 index 0000000..510701b --- /dev/null +++ b/lib/src/data/stores/user_data_store/memory_user_data_store.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Memory implementation of [UserDataStore] +class MemoryUserDataStore implements UserDataStore { + final String _tag = '$MemoryUserDataStore'; + + MemoryUserDataStore(); + + final _users = >{}; + + @override + FutureOr getUser(String userId) async { + print('$_tag:getUser($userId)'); + + final CacheModel cacheModel = _users[userId]; + return (cacheModel != null && !cacheModel.isExpired()) + ? cacheModel.model + : null; + } + + @override + FutureOr setUser(UserDataModel userModel) async { + print('$_tag:setUser($userModel)'); + + final DateTime expiration = + generateExpirationDateTime(Duration(minutes: 1)); + final cacheModel = + CacheModel(model: userModel, expiration: expiration); + _users[userModel.id] = cacheModel; + + return cacheModel.model; + } + + @override + FutureOr> getUsers({ + Cursor cursor = const Cursor(), + }) { + print('$_tag:getUsers'); + return _users.values.map((value) => value.model).toList(); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/data/stores/user_data_store/user_data_store.dart b/lib/src/data/stores/user_data_store/user_data_store.dart new file mode 100644 index 0000000..407dbaa --- /dev/null +++ b/lib/src/data/stores/user_data_store/user_data_store.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +abstract class UserDataStore { + FutureOr getUser(String userId); + + FutureOr setUser(UserDataModel userModel); + + FutureOr> getUsers({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/data/stores/user_data_store/user_data_store_factory.dart b/lib/src/data/stores/user_data_store/user_data_store_factory.dart new file mode 100644 index 0000000..289e12c --- /dev/null +++ b/lib/src/data/stores/user_data_store/user_data_store_factory.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/data.dart'; + +class UserDataStoreFactory { + final UserDataStore cloudDataStore; + final UserDataStore memoryDataStore; + + UserDataStoreFactory({ + @required this.cloudDataStore, + @required this.memoryDataStore, + }) : assert(cloudDataStore != null, 'No cloud $UserDataStore given'), + assert(memoryDataStore != null, 'No memory $UserDataStore given'); + + UserDataStore get create => cloudDataStore; + + @override + String toString() => '$runtimeType{ ' + 'memoryDataStore: $memoryDataStore, ' + 'cloudDataStore: $cloudDataStore' + ' }'; +} diff --git a/lib/src/data/utils/utils.dart b/lib/src/data/utils/utils.dart new file mode 100644 index 0000000..946e104 --- /dev/null +++ b/lib/src/data/utils/utils.dart @@ -0,0 +1,9 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +DateTime generateExpirationDateTime(Duration duration) { + return DateTime.now().add(duration); +} + +Cursor checkCursor(Cursor cursor) { + return cursor ??= Cursor(); +} diff --git a/lib/src/domain/entities/auth_entity.dart b/lib/src/domain/entities/auth_entity.dart new file mode 100644 index 0000000..a2c0c57 --- /dev/null +++ b/lib/src/domain/entities/auth_entity.dart @@ -0,0 +1,6 @@ +abstract class AuthEntity { + String accessToken; + String refreshToken; + DateTime accessTokenExpiration; + String tokenType; +} diff --git a/lib/src/domain/entities/base_entity.dart b/lib/src/domain/entities/base_entity.dart new file mode 100644 index 0000000..54eda23 --- /dev/null +++ b/lib/src/domain/entities/base_entity.dart @@ -0,0 +1,14 @@ +abstract class BaseEntity { + String id; + DateTime createdAt; + DateTime updatedAt; + int version; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/domain/entities/element_model.dart b/lib/src/domain/entities/element_model.dart new file mode 100644 index 0000000..49ad97b --- /dev/null +++ b/lib/src/domain/entities/element_model.dart @@ -0,0 +1,11 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class ElementEntity extends BaseEntity { + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/domain/entities/entry_entity.dart b/lib/src/domain/entities/entry_entity.dart new file mode 100644 index 0000000..774d750 --- /dev/null +++ b/lib/src/domain/entities/entry_entity.dart @@ -0,0 +1,26 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class EntryEntity extends ElementEntity { + String name; + String type; + dynamic content; + String startDate; + String endDate; + String location; + String ownerId; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'type: $type, ' + 'content: $content, ' + 'startDate: $startDate, ' + 'endDate: $endDate, ' + 'location: $location, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/domain/entities/group_entity.dart b/lib/src/domain/entities/group_entity.dart new file mode 100644 index 0000000..9d05d26 --- /dev/null +++ b/lib/src/domain/entities/group_entity.dart @@ -0,0 +1,20 @@ +import 'package:social_cv_client_flutter/data.dart'; + +class GroupEntity extends ElementDataModel { + String name; + List entryIds; + String type; + String ownerId; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'type: $type, ' + 'entryIds: $entryIds, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/domain/entities/part_entity.dart b/lib/src/domain/entities/part_entity.dart new file mode 100644 index 0000000..337f59a --- /dev/null +++ b/lib/src/domain/entities/part_entity.dart @@ -0,0 +1,17 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class PartEntity extends ElementEntity { + String name; + List groupIds; + String type; + String ownerId; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'groupIds: $groupIds, ' + 'type: $type, ' + 'owner: $ownerId' + ' }'; +} diff --git a/lib/src/domain/entities/profile_entity.dart b/lib/src/domain/entities/profile_entity.dart new file mode 100644 index 0000000..53f0bef --- /dev/null +++ b/lib/src/domain/entities/profile_entity.dart @@ -0,0 +1,23 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +class ProfileEntity extends ElementEntity { + String title; + String subtitle; + Uri picture; + Uri cover; + List partIds; + String type; + String ownerId; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'title: $title, ' + 'subtitle: $subtitle, ' + 'picture: $picture, ' + 'cover: $cover, ' + 'partIds: $partIds, ' + 'type: $type, ' + 'owner: $ownerId' + ' }'; +} diff --git a/lib/src/domain/entities/user_entity.dart b/lib/src/domain/entities/user_entity.dart new file mode 100644 index 0000000..808abe3 --- /dev/null +++ b/lib/src/domain/entities/user_entity.dart @@ -0,0 +1,21 @@ +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class UserEntity extends ElementEntity { + String email; + String username; + bool disabled; + String picture; + List profileIds; + dynamic permission; + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'disabled: $disabled, ' + 'email: $email, ' + 'username: $username, ' + 'picture: $picture, ' + 'profileIds: $profileIds, ' + 'permission: $permission' + ' }'; +} diff --git a/lib/src/domain/errors/commons_errors.dart b/lib/src/domain/errors/commons_errors.dart new file mode 100644 index 0000000..0347d5f --- /dev/null +++ b/lib/src/domain/errors/commons_errors.dart @@ -0,0 +1,19 @@ +class NotImplementedYetError extends Error { + final String message; + + NotImplementedYetError([this.message]); + + @override + String toString() => message != null + ? 'NotImplementedYetError: $message' + : 'NotImplementedYetError'; +} + +class NotSupportedError extends Error { + final String message; + + NotSupportedError(this.message); + + @override + String toString() => 'Unsupported operation: $message'; +} diff --git a/lib/src/domain/exceptions/app_exceptions.dart b/lib/src/domain/exceptions/app_exceptions.dart new file mode 100644 index 0000000..18f8115 --- /dev/null +++ b/lib/src/domain/exceptions/app_exceptions.dart @@ -0,0 +1,46 @@ +import 'package:meta/meta.dart'; + +abstract class AppException implements Exception { + final String message; + final StackTrace stackTrace; + final AppExceptionType type; + + const AppException({ + @required this.type, + this.message, + this.stackTrace, + }) : assert(type != null, 'No $AppExceptionType given'); +} + +enum AppExceptionType { + /// -------------------------------------------------------------------------- + /// Common + /// -------------------------------------------------------------------------- + somethingWentWrong, + + /// -------------------------------------------------------------------------- + /// Server + /// -------------------------------------------------------------------------- + serverSideProblem, + + /// -------------------------------------------------------------------------- + /// Authentication + /// -------------------------------------------------------------------------- + authNoToken, + authLoginFailed, + authRegistrationFailed, + authAccountAlreadyExists, + authAccountDisabled, + authUnauthorized, + authForbidden, + + /// -------------------------------------------------------------------------- + /// User + /// -------------------------------------------------------------------------- + userNotFound, + + /// -------------------------------------------------------------------------- + /// Form + /// -------------------------------------------------------------------------- + formPasswordWrongPolicy, +} diff --git a/lib/src/domain/exceptions/http_exceptions.dart b/lib/src/domain/exceptions/http_exceptions.dart new file mode 100644 index 0000000..afbf403 --- /dev/null +++ b/lib/src/domain/exceptions/http_exceptions.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta.dart'; + +class HttpException implements Exception { + final int statusCode; + final String statusMessage; + final StackTrace stackTrace; + + const HttpException({ + @required this.statusCode, + this.statusMessage, + this.stackTrace, + }) : assert(statusCode != null, 'No status code given'); +} diff --git a/lib/src/domain/repositories/app_preferences_repository.dart b/lib/src/domain/repositories/app_preferences_repository.dart new file mode 100644 index 0000000..d657578 --- /dev/null +++ b/lib/src/domain/repositories/app_preferences_repository.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +/// Repository interface for the application preferences +abstract class AppPrefsRepository { + /// Get App dark mode + /// + /// Must return the application dark mode [bool] or [null] if not found + FutureOr getDarkMode(); + + /// Set Application dark mode([bool]) + FutureOr toggleDarkMode(bool darkMode); + + /// Delete Application dark mode + FutureOr deleteDarkMode(); + + /// Delete all application preferences + FutureOr deleteAll(); + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/domain/repositories/auth_info_repository.dart b/lib/src/domain/repositories/auth_info_repository.dart new file mode 100644 index 0000000..f448eb0 --- /dev/null +++ b/lib/src/domain/repositories/auth_info_repository.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +/// Repository interface for authentication info purpose +abstract class AuthInfoRepository { + /// -------------------------------------------------------------------------- + /// Access Token + /// -------------------------------------------------------------------------- + + /// Set access token ([String]) + FutureOr setAccessToken(String token); + + /// Get access token + /// + /// Must return a access token ([String]) or [null] otherwise + FutureOr getAccessToken(); + + /// Delete access token + FutureOr deleteAccessToken(); + + /// Set access token expiration datetime as timestamp ([DateTime]) + FutureOr setAccessTokenExpiration(DateTime expiration); + + /// Get access token expiration time + /// + /// Must return the access token expiration datetime as timestamp ([int]) + /// or [null] otherwise + FutureOr getAccessTokenExpiration(); + + /// Delete access token expiration datetime + FutureOr deleteAccessTokenExpiration(); + + /// -------------------------------------------------------------------------- + /// Refresh Token + /// -------------------------------------------------------------------------- + + /// Set refresh token ([String]) + FutureOr setRefreshToken(String token); + + /// Get refresh token + /// + /// Must return a refresh token ([String]) or [null] otherwise + FutureOr getRefreshToken(); + + /// Delete refresh token + FutureOr deleteRefreshToken(); + + /// Set refresh token expiration datetime as timestamp ([int]) + FutureOr setRefreshTokenExpiration(DateTime expiration); + + /// Get refresh token expiration datetime + /// + /// Must return the access token expiration datetime ([DateTime]) + /// or [null] otherwise + FutureOr getRefreshTokenExpiration(); + + /// Delete refresh token expiration date + FutureOr deleteRefreshTokenExpiration(); +} diff --git a/lib/src/domain/repositories/entity_repository.dart b/lib/src/domain/repositories/entity_repository.dart new file mode 100644 index 0000000..b2f1c27 --- /dev/null +++ b/lib/src/domain/repositories/entity_repository.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/domain/entities/base_entity.dart'; + +abstract class EntityRepository { + /// Fetch the [T] identified by [id] + /// + /// [force] can be used to avoid cache use ([$false] by default) + /// + /// Must return an [T] + FutureOr getById( + String id, { + bool force = false, + }); + + /// Fetch [T] list + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [T] + FutureOr> getList({ + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + +// TODO: Add delete +// TODO: Add create +} diff --git a/lib/src/domain/repositories/entry_repository.dart b/lib/src/domain/repositories/entry_repository.dart new file mode 100644 index 0000000..7fa23b5 --- /dev/null +++ b/lib/src/domain/repositories/entry_repository.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/domain/repositories/entity_repository.dart'; + +/// Repository interface for entry element purpose +abstract class EntryRepository extends EntityRepository { + /// Fetch groups from the parent group identified by [groupId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [EntryEntity] + FutureOr> getEntriesFromGroup( + String groupId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + /// Fetch entries from the user identified by [userId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [EntryEntity] + FutureOr> getEntriesFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/domain/repositories/group_repository.dart b/lib/src/domain/repositories/group_repository.dart new file mode 100644 index 0000000..827e3a1 --- /dev/null +++ b/lib/src/domain/repositories/group_repository.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/domain/repositories/entity_repository.dart'; + +/// Repository interface for group element purpose +abstract class GroupRepository extends EntityRepository { + /// Fetch groups from the parent part identified by [partId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [GroupEntity] + FutureOr> getGroupsFromPart( + String partId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + /// Fetch groups from the user identified by [userId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [GroupEntity] + FutureOr> getGroupsFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/domain/repositories/identity_repository.dart b/lib/src/domain/repositories/identity_repository.dart new file mode 100644 index 0000000..d75f467 --- /dev/null +++ b/lib/src/domain/repositories/identity_repository.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/domain.dart'; + +/// Repository interface for connected user information purpose +abstract class IdentityRepository { + /// Fetch information of the authenticated user + /// + /// Must return an [UserEntity] + FutureOr getIdentity(); + +// /// Fetch profiles from the authenticated user account +// /// +// /// [Cursor] can be used to choose the offset and the limit +// /// +// /// Must return a [List] of [ProfileEntity] +// FutureOr> getProfilesFromIdentity({ +// /// TODO: Add filters +// /// TODO: Add sort +// Cursor cursor = const Cursor(), +// }); +} diff --git a/lib/src/domain/repositories/part_repository.dart b/lib/src/domain/repositories/part_repository.dart new file mode 100644 index 0000000..5738364 --- /dev/null +++ b/lib/src/domain/repositories/part_repository.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/domain/repositories/entity_repository.dart'; + +/// Repository interface for part element purpose +abstract class PartRepository extends EntityRepository { + /// Fetch parts from the parent profile identified by [profileId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [PartEntity] + FutureOr> getPartsFromProfile( + String profileId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); + + /// Fetch parts from the user identified by [userId] + /// + /// [Cursor] can be used to choose the offset and the limit + /// + /// Must return a [List] of [PartEntity] + FutureOr> getPartsFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/domain/repositories/profile_repository.dart b/lib/src/domain/repositories/profile_repository.dart new file mode 100644 index 0000000..9a332fa --- /dev/null +++ b/lib/src/domain/repositories/profile_repository.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/domain/repositories/entity_repository.dart'; + +/// Repository interface for profile element purpose +abstract class ProfileRepository extends EntityRepository { + FutureOr> getProfilesFromUser( + String userId, { + + /// TODO: Add filters + /// TODO: Add sort + Cursor cursor = const Cursor(), + }); +} diff --git a/lib/src/domain/repositories/user_repository.dart b/lib/src/domain/repositories/user_repository.dart new file mode 100644 index 0000000..ef83b9a --- /dev/null +++ b/lib/src/domain/repositories/user_repository.dart @@ -0,0 +1,5 @@ +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/domain/repositories/entity_repository.dart'; + +/// Repository interface for user element purpose +abstract class UserRepository extends EntityRepository {} diff --git a/lib/src/domain/services/cv_auth_service.dart b/lib/src/domain/services/cv_auth_service.dart new file mode 100644 index 0000000..14566bf --- /dev/null +++ b/lib/src/domain/services/cv_auth_service.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// Service for authentication purpose +abstract class CVAuthService { + /// -------------------------------------------------------------------------- + /// Auth + /// -------------------------------------------------------------------------- + + /// Authenticate + /// + /// Must use [email] and [password] to authenticate an user + /// + /// Must return an [AuthDataModel] + FutureOr authenticate({ + @required String email, + @required String password, + }); + + /// Register + /// + /// Must use [fName], [lName], [email] and [password] to register an user + /// + /// Must return an [AuthDataModel] + FutureOr register({ + @required String fName, + @required String lName, + @required String email, + @required String password, + }); + + FutureOr logout(); +} diff --git a/lib/src/domain/services/foundation_config_service.dart b/lib/src/domain/services/foundation_config_service.dart new file mode 100644 index 0000000..08ecf4e --- /dev/null +++ b/lib/src/domain/services/foundation_config_service.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +abstract class FoundationConfigService { + /// Get Api server url ([String]) + FutureOr getApiServerUrl(); + + /// Get client id ([String]) + FutureOr getClientId(); + + /// Get client secret ([String]) + FutureOr getClientSecret(); +} diff --git a/lib/src/localizations/cv_localization.dart b/lib/src/localizations/cv_localization.dart deleted file mode 100644 index b7d24b9..0000000 --- a/lib/src/localizations/cv_localization.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization_en.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization_fr.dart'; - -abstract class CVLocalizations { - static CVLocalizations of(BuildContext context) { - return Localizations.of(context, CVLocalizations); - } - - /// App - String get appName; - - /// Auth Page - String get authTitle; - - /// Auth Stuff - String get authNoEmailTitle; - - String get authNotEmailExplain; - - String get authNoEmailExplain; - - String get authNoPasswordTitle; - - String get authNotPasswordExplain; - - String get authNoPasswordExplain; - - String get authCreateYourAccount; - - String get authSignUp; - - String get authSignUpCTA; - - String get authSignUpSucceed; - - String get authSignUpFailed; - - String get authSignIn; - - String get authSignInCTA; - - String get authSignInGoogleCTA; - - String get authSignInFacebookCTA; - - String get authSignInSucceed; - - String get authSignInFailed; - - String get authLogout; - - String get authLogoutCTA; - - String get authLogoutSucceed; - - String get authLogoutFailed; - - String get authPrivacyExplain; - - String get authPrivacyReadCTA; - - String get authAlreadyHaveAccountCTA; - - String get authForgotPasswordCTA; - - String get authNoAccountCTA; - - String get authOr; - - /// Home Page - String get homeTitle; - - String get homeWelcome; - - /// Account Page - String get accountMyProfile; - - /// Profile Page - String get profileTitle; - - /// Settings Page - String get settingsTitle; - - String get settingsThemeCTA; - - String get settingsThemeDefault; - - String get settingsThemeLight; - - String get settingsThemeDark; - - /// Search Page - String get searchTitle; - - String get searchSearchBarHint; - - /// Profile Widget - String get profileWidgetDetails; - - /// Profile Widget List - String get profileListOptions; - - String get profileListSorting; - - String get profileListItemPerPage; - - String get profileListLoadMore; - - /// Part Widget - String get partWidgetDetails; - - /// Part Widget List - String get partListOptions; - - String get partListSorting; - - String get partListItemPerPage; - - String get partListLoadMore; - - /// Group Widget - String get groupWidgetDetails; - - /// Group Widget List - String get groupListOptions; - - String get groupListSorting; - - String get groupListItemPerPage; - - String get groupListLoadMore; - - /// Entry Widget - String get entryWidgetDetails; - - /// Entry Widget List - String get entryListOptions; - - String get entryListSorting; - - String get entryListItemPerPage; - - String get entryListLoadMore; - - /// Sort Dialog - String get sortDialogCancel; - - String get sortDialogConfirm; - - /// Exception Error - String get exceptionFormatException; - - String get exceptionTimeoutException; - - ///Api Error - String get apiErrorWrongPasswordError; - - String get apiErrorUserNotFoundError; - - ///Server Error : HTTP 400 - String get httpClientErrorBadRequest; - - String get httpClientErrorPaymentRequired; - - String get httpClientErrorForbidden; - - String get httpClientErrorNotFound; - - String get httpClientErrorMethodNotAllowed; - - String get httpClientErrorNotAcceptable; - - String get httpClientErrorRequestTimeout; - - String get httpClientErrorConflict; - - String get httpClientErrorGone; - - String get httpClientErrorLengthRequired; - - String get httpClientErrorPayloadTooLarge; - - String get httpClientErrorURITooLong; - - String get httpClientErrorUnsupportedMediaType; - - String get httpClientErrorExpectationFailed; - - String get httpClientErrorUpgradeRequired; - - ///Server Error : HTTP 500 - String get httpServerErrorInternalServerError; - - String get httpServerErrorNotImplemented; - - String get httpServerErrorBadGateway; - - String get httpServerErrorServiceUnavailable; - - String get httpServerErrorGatewayTimeout; - - String get httpServerErrorHttpVersionNotSupported; - - ///Menu Widget - - String get menuPPCTA; - - String get menuToSCTA; - - ///Others - String get middleDot; - - String get username; - - String get email; - - String get password; - - String get passwordRepeat; - - String get token; - - String get cancelCTA; - - String get settingsCTA; - - String get account; - - String get home; - - String get resume; - - String get profile; - - String get search; - - String get history; - - String get loadMore; - - String get errorOccurred; - - String get retryCTA; - - String get yesCTA; - - String get noCTA; - - String get moreCTA; - - String get notYetImplemented; - - String get notSupported; -} - -class CVLocalizationsDelegate extends LocalizationsDelegate { - const CVLocalizationsDelegate(); - - @override - bool isSupported(Locale locale) => ['en', 'fr'].contains(locale.languageCode); - - @override - Future load(Locale locale) { - final String name = - (locale.countryCode == null || locale.countryCode.isEmpty) - ? locale.languageCode - : locale.toString(); - final String localeName = Intl.canonicalizedLocale(name); - Intl.defaultLocale = localeName; - - if (locale.languageCode == 'fr') { - return CVLocalizationsFR.load(locale); - } else { - return CVLocalizationsEN.load(locale); - } - } - - @override - bool shouldReload(CVLocalizationsDelegate old) => false; - - @override - String toString() => 'DefaultCVLocalizations.delegate(en_US)'; -} diff --git a/lib/src/localizations/cv_localization_en.dart b/lib/src/localizations/cv_localization_en.dart deleted file mode 100644 index 4427901..0000000 --- a/lib/src/localizations/cv_localization_en.dart +++ /dev/null @@ -1,376 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; - -class CVLocalizationsEN implements CVLocalizations { - const CVLocalizationsEN(); - - /// App - @override - String get appName => 'Social CV'; - - /// Auth Page - @override - String get authTitle => 'Connection'; - - @override - String get authNoEmailTitle => 'Empty email'; - - @override - String get authNotEmailExplain => 'Please enter a real e-mail.'; - - @override - String get authNoEmailExplain => 'Please provide an email'; - - @override - String get authNoPasswordTitle => 'Empty password'; - - @override - String get authNoPasswordExplain => 'Please provide a password'; - - @override - String get authCreateYourAccount => 'Create your account'; - - @override - String get authPrivacyExplain => - 'Like privacy? We feel you. We don’t use or sell your data.'; - - @override - String get authPrivacyReadCTA => 'Touch to read our privacy policy.'; - - @override - String get authAlreadyHaveAccountCTA => 'Already have an account? Sign-in'; - - @override - String get authNotPasswordExplain => 'This is not a password'; - - @override - String get authSignUp => 'Register'; - - @override - String get authSignUpCTA => 'Sign-up'; - - @override - String get authSignUpFailed => 'Registration failed!'; - - @override - String get authSignUpSucceed => 'Registration succeed!'; - - @override - String get authSignIn => 'Sign-in'; - - @override - String get authSignInCTA => 'Sign-in'; - - @override - String get authSignInGoogleCTA => 'Sign-in with Google'; - - @override - String get authSignInFacebookCTA => 'Sign-in with Facebook'; - - @override - String get authSignInSucceed => 'Login succeed!'; - - @override - String get authSignInFailed => 'Login failed!'; - - @override - String get authLogout => 'Logout'; - - @override - String get authLogoutCTA => 'Logout'; - - @override - String get authLogoutFailed => 'Logout failed!'; - - @override - String get authLogoutSucceed => 'Logout succeed!'; - - @override - String get authForgotPasswordCTA => 'Forgot password?'; - - @override - String get authNoAccountCTA => 'Don\'t have an account yet? Sign-up'; - - @override - String get authOr => 'OR'; - - ///Home Page - @override - String get homeTitle => 'Social CV'; - - @override - String get homeWelcome => 'Welcome on our new resume social network !'; - - /// Account Page - @override - String get accountMyProfile => 'My profiles'; - - /// Profile Page - @override - String get profileTitle => 'Profile'; - - /// Settings Page - @override - String get settingsTitle => 'Settings'; - - @override - String get settingsThemeCTA => 'Dark Mode'; - - @override - String get settingsThemeDefault => 'Default'; - - @override - String get settingsThemeLight => 'Light'; - - @override - String get settingsThemeDark => 'Dark'; - - /// Search Page - @override - String get searchTitle => 'Search'; - - @override - String get searchSearchBarHint => 'Search resume...'; - - /// Profile Widget - String get profileWidgetDetails => 'Profile details'; - - /// Profile Widget List - String get profileListOptions => 'Profile options'; - - String get profileListSorting => 'Sorting Profiles'; - - String get profileListItemPerPage => 'Profile per page'; - - String get profileListLoadMore => 'Load more profiles'; - - /// Part Widget - String get partWidgetDetails => 'Part détails'; - - /// Part Widget List - @override - String get partListOptions => 'Part list options'; - - @override - String get partListSorting => 'Sorting parts'; - - @override - String get partListItemPerPage => 'Parts per page'; - - @override - String get partListLoadMore => 'Load more parts'; - - /// Group Widget - String get groupWidgetDetails => 'Group détails'; - - /// Group Widget List - @override - String get groupListOptions => 'Group list options'; - - @override - String get groupListSorting => 'Sorting groups'; - - @override - String get groupListItemPerPage => 'Groups per page'; - - @override - String get groupListLoadMore => 'Load more groups'; - - /// Entry Widget - String get entryWidgetDetails => 'Entry détails'; - - /// Entry Widget List - @override - String get entryListOptions => 'Entry list options'; - - @override - String get entryListSorting => 'Sorting entries'; - - @override - String get entryListItemPerPage => 'Entries per page'; - - @override - String get entryListLoadMore => 'Load more entries'; - - /// Sort Dialog - @override - String get sortDialogCancel => 'Cancel'; - - @override - String get sortDialogConfirm => 'Confirm'; - - /// Menu Widget - @override - String get menuPPCTA => 'Privacy Policy'; - - @override - String get menuToSCTA => 'Terms of Service'; - - /// Exception Error - @override - String get exceptionFormatException => 'Exception : Wrong Format'; - - @override - String get exceptionTimeoutException => 'Exception : Request Timeout'; - - /// Api Error - @override - String get apiErrorWrongPasswordError => 'Wrong password'; - - @override - String get apiErrorUserNotFoundError => 'User not found'; - - /// Server Error : HTTP 400 - @override - String get httpClientErrorBadRequest => 'Bad request'; - - @override - String get httpClientErrorPaymentRequired => 'Payment required'; - - @override - String get httpClientErrorForbidden => 'Forbidden'; - - @override - String get httpClientErrorNotFound => 'Not found'; - - @override - String get httpClientErrorMethodNotAllowed => 'Not allowed'; - - @override - String get httpClientErrorNotAcceptable => 'Not acceptable'; - - @override - String get httpClientErrorRequestTimeout => 'Request timeout'; - - @override - String get httpClientErrorConflict => 'Conflict'; - - @override - String get httpClientErrorGone => 'Gone'; - - @override - String get httpClientErrorLengthRequired => 'Length required'; - - @override - String get httpClientErrorPayloadTooLarge => 'Payload too large'; - - @override - String get httpClientErrorURITooLong => 'URI too long'; - - @override - String get httpClientErrorUnsupportedMediaType => 'Unsupported media type'; - - @override - String get httpClientErrorExpectationFailed => 'Expectation Failed'; - - @override - String get httpClientErrorUpgradeRequired => 'Upgrade required'; - - /// Server Error : HTTP 500 - @override - String get httpServerErrorInternalServerError => 'Internal Server Error'; - - @override - String get httpServerErrorNotImplemented => 'Not implemented'; - - @override - String get httpServerErrorBadGateway => 'Bad Gateway'; - - @override - String get httpServerErrorServiceUnavailable => 'Service Unavailable'; - - @override - String get httpServerErrorGatewayTimeout => 'Gateway Timeout'; - - @override - String get httpServerErrorHttpVersionNotSupported => - 'HTTP Version Not Supported'; - - /// Others - @override - String get middleDot => '·'; - - @override - String get username => 'Username'; - - @override - String get email => 'Email'; - - @override - String get password => 'Password'; - - @override - String get passwordRepeat => 'Repeat Password'; - - @override - String get token => 'Token'; - - @override - String get cancelCTA => 'Cancel'; - - @override - String get settingsCTA => 'Settings'; - - @override - String get account => 'Account'; - - @override - String get home => 'Home'; - - @override - String get resume => 'Resume'; - - @override - String get profile => 'Profile'; - - @override - String get search => 'Search'; - - @override - String get history => 'History'; - - @override - String get loadMore => 'Load more'; - - @override - String get errorOccurred => 'An error occurred'; - - @override - String get retryCTA => 'Retry'; - - @override - String get yesCTA => 'Yes'; - - @override - String get noCTA => 'No'; - - @override - String get moreCTA => 'More'; - - @override - String get notYetImplemented => 'Not yet implemented'; - - @override - String get notSupported => 'Not supported'; - - /// Creates an object that provides US English resource values for the - /// application. - /// - /// The [locale] parameter is ignored. - /// - /// This method is typically used to create a [LocalizationsDelegate]. - /// The [MaterialApp] does so by default. - static Future load(Locale locale) { - return SynchronousFuture(const CVLocalizationsEN()); - } - - /// A [LocalizationsDelegate] that uses [CVLocalizationsEN.load] - /// to create an instance of this class. - /// - /// [MaterialApp] automatically adds this value to [MaterialApp.localizationsDelegates]. - static const LocalizationsDelegate delegate = - CVLocalizationsDelegate(); -} diff --git a/lib/src/localizations/cv_localization_fr.dart b/lib/src/localizations/cv_localization_fr.dart deleted file mode 100644 index f510c52..0000000 --- a/lib/src/localizations/cv_localization_fr.dart +++ /dev/null @@ -1,381 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; - -class CVLocalizationsFR implements CVLocalizations { - const CVLocalizationsFR(); - - @override - String get appName => 'Social CV'; - - /// Auth Page - @override - String get authTitle => 'Connexion'; - - @override - String get authNoEmailTitle => 'E-mail vide'; - - @override - String get authNotEmailExplain => 'Merci de renseigner un e-mail existant.'; - - @override - String get authNoEmailExplain => 'Merci de renseigner un e-mail'; - - @override - String get authNoPasswordTitle => 'Mot de passe vide'; - - @override - String get authNoPasswordExplain => 'Merci de renseigner un mot de passe'; - - @override - String get authCreateYourAccount => 'Créez votre compte'; - - @override - String get authSignUp => 'Inscription'; - - @override - String get authSignUpCTA => 'S\'inscrire'; - - @override - String get authSignUpFailed => 'Inscription échoué !'; - - @override - String get authSignUpSucceed => 'Inscription réussite !'; - - @override - String get authSignIn => 'Connectez vous avec votre compte'; - - @override - String get authSignInCTA => 'Se connecter'; - - @override - String get authSignInGoogleCTA => 'Se connecter avec Google'; - - @override - String get authSignInFacebookCTA => 'Se connecter avec Facebook'; - - @override - String get authSignInFailed => 'Connexion échoué !'; - - @override - String get authLogout => 'Se déconnecter'; - - @override - String get authLogoutCTA => 'Se déconnecter'; - - @override - String get authLogoutFailed => 'Déconnexion échoué !'; - - @override - String get authLogoutSucceed => 'Déconnexion réussite !'; - - @override - String get authPrivacyExplain => - 'Vous aimez votre vie privée ? Nous le savons. Nous n\'utilisons, ni ' - 'vendons vos données.'; - - @override - String get authPrivacyReadCTA => - 'Touchez ici pour lire notre politique de confidentialité.'; - - @override - String get authAlreadyHaveAccountCTA => - 'Vous avez déjà un compte ? Connectez-vous'; - - @override - String get authForgotPasswordCTA => 'Mot de passe oublié ?'; - - @override - String get authNoAccountCTA => 'Vous n\'avez pas de compte ? Inscrivez-vous'; - - @override - String get authNotPasswordExplain => 'Ceci n\'est pas un mot de passe'; - - @override - String get authOr => 'OU'; - - /// Home Page - @override - String get homeTitle => 'Social CV'; - - @override - String get homeWelcome => 'Bienvenue sur notre nouveau réseau social de CV !'; - - /// Account Page - @override - String get accountMyProfile => 'Mes profils'; - - /// Profile Page - @override - String get profileTitle => 'Profil'; - - /// Settings Pages - - @override - String get settingsTitle => 'Paramètres'; - - @override - String get settingsCTA => 'Paramètres'; - - @override - String get settingsThemeCTA => 'Mode Sombre'; - - @override - String get settingsThemeDefault => 'Défaut'; - - @override - String get settingsThemeLight => 'Claire'; - - @override - String get settingsThemeDark => 'Sombre'; - - /// Search Page - @override - String get searchTitle => 'Recherche'; - - @override - String get searchSearchBarHint => 'Rechercher un profil ...'; - - /// Profile Widget - String get profileWidgetDetails => 'Détails du profile'; - - ///Profile Widget List - String get profileListOptions => 'Options profiles'; - - String get profileListSorting => 'Trier Profiles'; - - String get profileListItemPerPage => 'Profils par page'; - - String get profileListLoadMore => 'Charger plus de profiles'; - - /// Part Widget - String get partWidgetDetails => 'Détails de la partie'; - - /// Part Widget List - @override - String get partListOptions => 'Options'; - - @override - String get partListSorting => 'Trier les parties'; - - @override - String get partListItemPerPage => 'Parties par page'; - - @override - String get partListLoadMore => 'charger plus de parties'; - - /// Group Widget - String get groupWidgetDetails => 'Détails du groupe'; - - /// Group Widget List - @override - String get groupListOptions => 'Options'; - - @override - String get groupListSorting => 'Trier les groupes'; - - @override - String get groupListItemPerPage => 'Groupes par page'; - - @override - String get groupListLoadMore => 'charger plus de groupes'; - - /// Entry Widget - String get entryWidgetDetails => 'Détails de l\'entrée'; - - /// Entry Widget List - @override - String get entryListOptions => 'Options'; - - @override - String get entryListSorting => 'Trier les entrées'; - - @override - String get entryListItemPerPage => 'Entrées par page'; - - @override - String get entryListLoadMore => 'Charger plus de entrées'; - - /// Sort Dialog - @override - String get sortDialogCancel => 'Annuler'; - - @override - String get sortDialogConfirm => 'Valider'; - - /// Exception Error - @override - String get exceptionFormatException => 'Exception : Mauvais Format'; - - @override - String get exceptionTimeoutException => 'Exception : Requete Expiré'; - - /// Api Error - @override - String get apiErrorWrongPasswordError => 'Mauvais mot de passe'; - - @override - String get apiErrorUserNotFoundError => 'Utilisateur introuvable'; - - /// Server Error : HTTP 400 - @override - String get httpClientErrorBadRequest => 'Mauvaise requete'; - - @override - String get httpClientErrorPaymentRequired => 'Paiement requis'; - - @override - String get httpClientErrorForbidden => 'Interdit'; - - @override - String get httpClientErrorNotFound => 'Introuvable'; - - @override - String get httpClientErrorMethodNotAllowed => 'Non autorisé'; - - @override - String get httpClientErrorNotAcceptable => 'Non acceptable'; - - @override - String get httpClientErrorRequestTimeout => 'Requete expirée'; - - @override - String get httpClientErrorConflict => 'Conflit'; - - @override - String get httpClientErrorGone => 'Disparu'; - - @override - String get httpClientErrorLengthRequired => 'Taille requise'; - - @override - String get httpClientErrorPayloadTooLarge => 'Payload trop large'; - - @override - String get httpClientErrorURITooLong => 'Lien trop long'; - - @override - String get httpClientErrorUnsupportedMediaType => - 'Type de média non supporté'; - - @override - String get httpClientErrorExpectationFailed => 'Échoué'; - - @override - String get httpClientErrorUpgradeRequired => 'Mise à jour requise'; - - /// Server Error : HTTP 500 - @override - String get httpServerErrorInternalServerError => 'Erreur Serveur interne'; - - @override - String get httpServerErrorNotImplemented => 'Non implementé'; - - @override - String get httpServerErrorBadGateway => 'Mauvaise passerelle'; - - @override - String get httpServerErrorServiceUnavailable => 'Service non disponible '; - - @override - String get httpServerErrorGatewayTimeout => 'Temps ecoulé'; - - @override - String get httpServerErrorHttpVersionNotSupported => - 'Version HTTP non supporté'; - - /// Menu Widget - - @override - String get menuPPCTA => 'Politique de confidentialité'; - - @override - String get menuToSCTA => 'Termes de Service'; - - /// Others - @override - String get middleDot => '·'; - - @override - String get username => 'Nom d\'utilisateur'; - - @override - String get email => 'Email'; - - @override - String get password => 'Mot de passe'; - - @override - String get passwordRepeat => 'Password répété'; - - @override - String get token => 'Jeton'; - - @override - String get cancelCTA => 'Annuler'; - - @override - String get account => 'Compte'; - - @override - String get home => 'Accueil'; - - @override - String get resume => 'CV'; - - @override - String get profile => 'Profil'; - - @override - String get search => 'Rechercher'; - - @override - String get history => 'Historique'; - - @override - String get loadMore => 'Charger plus'; - - @override - String get errorOccurred => 'Une erreur s\'est produite'; - - @override - String get retryCTA => 'Re-éssayer'; - - @override - String get yesCTA => 'Oui'; - - @override - String get noCTA => 'Non'; - - @override - String get authSignInSucceed => 'Connecté'; - - @override - String get moreCTA => 'Plus'; - - @override - String get notYetImplemented => 'Pas encore implémenté'; - - @override - String get notSupported => 'Non supporté'; - - /// Creates an object that provides US English resource values for the - /// application. - /// - /// The [locale] parameter is ignored. - /// - /// This method is typically used to create a [LocalizationsDelegate]. - /// The [MaterialApp] does so by default. - static Future load(Locale locale) { - return SynchronousFuture(const CVLocalizationsFR()); - } - - /// A [LocalizationsDelegate] that uses [CVLocalizationsFR.load] - /// to create an instance of this class. - /// - /// [MaterialApp] automatically adds this value to [MaterialApp.localizationsDelegates]. - static const LocalizationsDelegate delegate = - CVLocalizationsDelegate(); -} diff --git a/lib/src/models/view_models.dart b/lib/src/models/view_models.dart deleted file mode 100644 index 021e072..0000000 --- a/lib/src/models/view_models.dart +++ /dev/null @@ -1,2 +0,0 @@ -/// TODO : Added all ViewModels -/// TODO : Repositories have to be base on these ViewModels diff --git a/lib/src/pages/account_page.dart b/lib/src/pages/account_page.dart deleted file mode 100644 index ba60af4..0000000 --- a/lib/src/pages/account_page.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/repositories/repositories_provider.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/profile_list_widget.dart'; - -class AccountPage extends StatelessWidget { - const AccountPage({Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - logger.info('Building AccountPage'); - - AccountBloc _accountBloc = BlocProvider.of(context); - - return SafeArea( - left: false, - right: false, - child: Stack( - children: [ - StreamBuilder( - stream: _accountBloc.isFetchingAccountDetailsStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - _AccountPageDetails(), - ], - ), - ); - } -} - -class _AccountPageDetails extends StatelessWidget { - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - - return StreamBuilder( - stream: _accountBloc.isAuthenticatedStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (snapshot.data == true) return _AccountPageDetailsConnected(); - if (snapshot.data == false) return _AccountPageDetailsNotConnected(); - } else if (snapshot.hasError) { - return Container( - child: - ErrorContent(message: translateError(context, snapshot.error)), - ); - } - return Container(); - }, - ); - } -} - -class _AccountPageDetailsNotConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Center( - child: RaisedButton( - child: Text(CVLocalizations.of(context).authSignInCTA), - onPressed: () => navigateToLogin(context), - ), - ); - } -} - -class _AccountPageDetailsConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - RepositoriesProvider _repositories = RepositoriesProvider.of(context); - - return StreamBuilder( - stream: _accountBloc.accountDetailsStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return ListView( - children: [ - ExpansionTile( - leading: Icon(MdiIcons.accountBoxMultiple), - title: Text(CVLocalizations.of(context).accountMyProfile), - children: [ - BlocProvider( - bloc: ProfileListBloc( - cvRepository: _repositories.cvRepository, - preferencesRepository: - _repositories.preferencesRepository, - ), - child: ProfileListWidget( - fromUserModel: snapshot.data, - showOptions: false, - shrinkWrap: true, - physics: ClampingScrollPhysics(), - ), - ), - ], - ), - ], - ); - } else if (snapshot.hasError) { - return ErrorCard(message: translateError(context, snapshot.error)); - } - return Container(); - }, - ); - } -} diff --git a/lib/src/pages/auth_page.dart b/lib/src/pages/auth_page.dart deleted file mode 100644 index 6d014ff..0000000 --- a/lib/src/pages/auth_page.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/colors.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/widgets/login_form_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/register_form_widget.dart'; - -class AuthPage extends StatefulWidget { - @override - State createState() => _AuthPageState(); -} - -class _AuthPageState extends State - with SingleTickerProviderStateMixin { - static const String _TAG = '_AuthPageState'; - - final GlobalKey _scaffoldKey = new GlobalKey(); - - PageController _pageController; - - Color left = Colors.black; - Color right = Colors.white; - - @override - Widget build(BuildContext context) { - logger.info('$_TAG:build'); - - AccountBloc _accountBloc = BlocProvider.of(context); - - return Scaffold( - key: _scaffoldKey, - -// appBar: AppBar( -// title: Text(CVLocalizations.of(context).authTitle), -// centerTitle: true, -// ), - body: Stack( - children: [ - StreamBuilder( - stream: _accountBloc.isLoggingStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - NotificationListener( - onNotification: (overscroll) { - overscroll.disallowGlow(); - }, - child: SingleChildScrollView( - child: Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height >= 775.0 - ? MediaQuery.of(context).size.height - : 775.0, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppColors.loginGradientStart, - AppColors.loginGradientEnd - ], - begin: const FractionalOffset(0.0, 0.0), - end: const FractionalOffset(1.0, 1.0), - stops: [0.0, 1.0], - tileMode: TileMode.clamp, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.only(top: 75.0), - child: Image( - width: 250.0, - height: 191.0, - fit: BoxFit.fill, - image: new AssetImage('assets/img/login_logo.png')), - ), - Padding( - padding: EdgeInsets.only(top: 20.0), - child: _buildMenuBar(context), - ), - Expanded( - flex: 2, - child: PageView( - controller: _pageController, - onPageChanged: (i) { - if (i == 0) { - setState(() { - right = Colors.white; - left = Colors.black; - }); - } else if (i == 1) { - setState(() { - right = Colors.black; - left = Colors.white; - }); - } - }, - children: [ - new ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: LoginFormWidget(), - ), - new ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: RegisterFormWidget(), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - @override - void dispose() { - _pageController?.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - -// SystemChrome.setPreferredOrientations([ -// DeviceOrientation.portraitUp, -// DeviceOrientation.portraitDown, -// ]); - - _pageController = PageController(); - } - - Widget _buildMenuBar(BuildContext context) { - return Container( - width: 300.0, - height: 50.0, - decoration: BoxDecoration( - color: Color(0x552B2B2B), - borderRadius: BorderRadius.all(Radius.circular(25.0)), - ), - child: CustomPaint( - painter: TabIndicationPainter(pageController: _pageController), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: FlatButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onPressed: _onSignInButtonPress, - child: Text( - CVLocalizations.of(context).authSignIn, - style: TextStyle( - color: left, - fontSize: 16.0, - ), - ), - ), - ), - //Container(height: 33.0, width: 1.0, color: Colors.white), - Expanded( - child: FlatButton( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onPressed: _onSignUpButtonPress, - child: Text( - CVLocalizations.of(context).authSignUp, - style: TextStyle( - color: right, - fontSize: 16.0, - ), - ), - ), - ), - ], - ), - ), - ); - } - - void _onSignInButtonPress() { - _pageController.animateToPage(0, - duration: Duration(milliseconds: 500), curve: Curves.decelerate); - } - - void _onSignUpButtonPress() { - _pageController?.animateToPage(1, - duration: Duration(milliseconds: 500), curve: Curves.decelerate); - } -} - -class TabIndicationPainter extends CustomPainter { - Paint painter; - final double dxTarget; - final double dxEntry; - final double radius; - final double dy; - - final PageController pageController; - - TabIndicationPainter( - {this.dxTarget = 125.0, - this.dxEntry = 25.0, - this.radius = 21.0, - this.dy = 25.0, - this.pageController}) - : super(repaint: pageController) { - painter = new Paint() - ..color = Color(0xFFFFFFFF) - ..style = PaintingStyle.fill; - } - - @override - void paint(Canvas canvas, Size size) { - final pos = pageController.position; - double fullExtent = - (pos.maxScrollExtent - pos.minScrollExtent + pos.viewportDimension); - - double pageOffset = pos.extentBefore / fullExtent; - - bool left2right = dxEntry < dxTarget; - Offset entry = new Offset(left2right ? dxEntry : dxTarget, dy); - Offset target = new Offset(left2right ? dxTarget : dxEntry, dy); - - Path path = new Path(); - path.addArc( - new Rect.fromCircle(center: entry, radius: radius), 0.5 * pi, 1 * pi); - path.addRect( - new Rect.fromLTRB(entry.dx, dy - radius, target.dx, dy + radius)); - path.addArc( - new Rect.fromCircle(center: target, radius: radius), 1.5 * pi, 1 * pi); - - canvas.translate(size.width * pageOffset, 0.0); - canvas.drawShadow(path, Color(0xFFfbab66), 3.0, true); - canvas.drawPath(path, painter); - } - - @override - bool shouldRepaint(TabIndicationPainter oldDelegate) => true; -} diff --git a/lib/src/pages/entry_page.dart b/lib/src/pages/entry_page.dart deleted file mode 100644 index 3dbabbb..0000000 --- a/lib/src/pages/entry_page.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/repositories/repositories_provider.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; - -class EntryPage extends StatelessWidget { - const EntryPage({ - Key key, - @required this.entryId, - }) : assert(entryId != null), - super(key: key); - - final String entryId; - - @override - Widget build(BuildContext context) { - EntryBloc _entryBloc = BlocProvider.of(context); - RepositoriesProvider _repositories = RepositoriesProvider.of(context); - - _entryBloc.fetchEntry(entryId); - - return Scaffold( - appBar: AppBar( - title: StreamBuilder( - stream: _entryBloc.entryStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return Text('Error : ${snapshot.error.toString()}'); - } else if (snapshot.hasData) { - EntryModel entryModel = snapshot.data; - return Text(entryModel.name); - } - return LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 0, - ); - }, - ), - ), - body: _EntryPageEntryBody(), - ); - } -} - -class _EntryPageEntryBody extends StatelessWidget { - @override - Widget build(BuildContext context) { - EntryBloc _entryBloc = BlocProvider.of(context); - - return Stack( - children: [ - StreamBuilder( - stream: _entryBloc.isFetchingEntryStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - StreamBuilder( - stream: _entryBloc.entryStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return ErrorCard( - message: translateError(context, snapshot.error)); - } else if (snapshot.hasData) { - EntryModel entryModel = snapshot.data; - - return ListView( - children: [ - ListTile( - title: Text('name'), - subtitle: Text(entryModel.name), - ), - ListTile( - title: Text('type'), - subtitle: Text(entryModel.type), - ), - ListTile( - title: Text('content'), - subtitle: Text(entryModel.content), - ), - ], - ); - } - return LoadingShadowContent( - numberOfContentLines: 2, - padding: EdgeInsets.all(10.0), - ); - }, - ), - ], - ); - } -} diff --git a/lib/src/pages/group_page.dart b/lib/src/pages/group_page.dart deleted file mode 100644 index fed30db..0000000 --- a/lib/src/pages/group_page.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/entry_list_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; - -class GroupPage extends StatelessWidget { - const GroupPage({ - Key key, - @required this.groupId, - }) : assert(groupId != null), - super(key: key); - - final String groupId; - - @override - Widget build(BuildContext context) { - GroupBloc _groupBloc = BlocProvider.of(context); - _groupBloc.fetchGroup(groupId); - - return Scaffold( - appBar: AppBar( - title: StreamBuilder( - stream: _groupBloc.groupStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return Text('Error : ${snapshot.error.toString()}'); - } else if (snapshot.hasData) { - GroupModel groupModel = snapshot.data; - return Text(groupModel.name); - } - return LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 0, - ); - }, - ), - ), - body: _GroupPageGroupBody(), - ); - } -} - -class _GroupPageGroupBody extends StatelessWidget { - @override - Widget build(BuildContext context) { - GroupBloc _profileGroupBloc = BlocProvider.of(context); - - return Stack( - children: [ - StreamBuilder( - stream: _profileGroupBloc.isFetchingGroupStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - StreamBuilder( - stream: _profileGroupBloc.groupStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return ErrorCard( - message: translateError(context, snapshot.error), - ); - } else if (snapshot.hasData) { - return BlocProvider( - bloc: EntryListBloc(), - child: EntryListWidget( - fromGroupModel: snapshot.data, - showOptions: true, - ), - ); - } - return LoadingShadowContent( - numberOfTitleLines: 0, - numberOfContentLines: 2, - padding: EdgeInsets.all(10.0), - ); - }, - ), - ], - ); - } -} diff --git a/lib/src/pages/main_page.dart b/lib/src/pages/main_page.dart deleted file mode 100644 index bb6652a..0000000 --- a/lib/src/pages/main_page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/blocs/main_bloc.dart'; -import 'package:social_cv_client_flutter/src/commons/tags.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/pages/account_page.dart'; -import 'package:social_cv_client_flutter/src/pages/home_page.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/menu_button_widget.dart'; - -class MainPage extends StatelessWidget { - const MainPage({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - logger.info('Building MainPage'); - return Scaffold( - appBar: AppBar( - title: Text(CVLocalizations.of(context).appName), - centerTitle: true, - actions: [ - MenuButton(), - ], - ), - body: _MainPageBody(), - floatingActionButton: FloatingActionButton.extended( - heroTag: kHeroSearchFAB, - icon: Icon(Icons.search), - label: Text(CVLocalizations.of(context).search), - foregroundColor: Colors.white, - onPressed: () => navigateToSearch(context), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - bottomNavigationBar: _MainPageBottomNavigationBar(), - ); - } -} - -class _MainPageBody extends StatelessWidget { - @override - Widget build(BuildContext context) { - HomePage _homePage = HomePage(); - AccountPage _accountPage = AccountPage(); - - MainBloc _mainBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _mainBloc.tabStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - child: Stack( - children: [ - Offstage( - offstage: snapshot.data != TabType.HOME_TAB, - child: _homePage, - ), - Offstage( - offstage: snapshot.data != TabType.ACCOUNT_TAB, - child: _accountPage, - ), - ], - ), - ); - } else { - return Container(); - } - }, - ); - } -} - -class _MainPageBottomNavigationBar extends StatelessWidget { - @override - Widget build(BuildContext context) { - MainBloc _mainBloc = BlocProvider.of(context); - return Theme( - data: Theme.of(context).copyWith( - ///sets the background color of the `BottomNavigationBar` - canvasColor: Theme.of(context).primaryColor, - - ///sets the active color of the `BottomNavigationBar` if `Brightness` is light - primaryColor: Theme.of(context).selectedRowColor, - textTheme: Theme.of(context).primaryTextTheme, - ), - - ///sets the inactive color of the `BottomNavigationBar` - child: StreamBuilder( - stream: _mainBloc.tabStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - var index = 0; - if (snapshot.hasData) { - if (snapshot.data == TabType.HOME_TAB) index = 0; - if (snapshot.data == TabType.ACCOUNT_TAB) index = 2; - } - return BottomNavigationBar( - currentIndex: index, - onTap: (index) { - if (index == 0) { - _mainBloc.changeTab(TabType.HOME_TAB); - } else if (index == 2) { - _mainBloc.changeTab(TabType.ACCOUNT_TAB); - } - }, - type: BottomNavigationBarType.fixed, - items: [ - BottomNavigationBarItem( - icon: Icon(MdiIcons.homeOutline), - activeIcon: Icon(MdiIcons.home), - title: Text(CVLocalizations.of(context).home), - ), - const BottomNavigationBarItem( - ///Fake item - icon: SizedBox(), - title: SizedBox(), - ), - BottomNavigationBarItem( - icon: Icon(MdiIcons.accountOutline), - activeIcon: Icon(MdiIcons.account), - title: Text(CVLocalizations.of(context).account), - ), - ], - ); - }, - ), - ); - } -} diff --git a/lib/src/pages/part_page.dart b/lib/src/pages/part_page.dart deleted file mode 100644 index 7c93d18..0000000 --- a/lib/src/pages/part_page.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/group_list_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; - -class PartPage extends StatelessWidget { - const PartPage({ - Key key, - @required this.partId, - }) : assert(partId != null), - super(key: key); - - final String partId; - - @override - Widget build(BuildContext context) { - PartBloc _partBloc = BlocProvider.of(context); - _partBloc.fetchPart(partId); - - return Scaffold( - appBar: AppBar( - title: StreamBuilder( - stream: _partBloc.partStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return Text('Error : ${snapshot.error.toString()}'); - } else if (snapshot.hasData) { - PartModel partModel = snapshot.data; - return Text(partModel.name); - } - return LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 0, - ); - }, - ), - ), - body: _PartPagePartBody(), - ); - } -} - -class _PartPagePartBody extends StatelessWidget { - @override - Widget build(BuildContext context) { - PartBloc _partBloc = BlocProvider.of(context); - - return Stack( - children: [ - StreamBuilder( - stream: _partBloc.isFetchingPartStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - StreamBuilder( - stream: _partBloc.partStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return ErrorCard( - message: translateError(context, snapshot.error), - ); - } else if (snapshot.hasData) { - return BlocProvider( - bloc: GroupListBloc(), - child: GroupListWidget( - fromPartModel: snapshot.data, - showOptions: true, - ), - ); - } - return LoadingShadowContent( - numberOfTitleLines: 0, - numberOfContentLines: 2, - padding: EdgeInsets.all(10.0), - ); - }, - ), - ], - ); - } -} diff --git a/lib/src/pages/profile_page.dart b/lib/src/pages/profile_page.dart deleted file mode 100644 index 75447ba..0000000 --- a/lib/src/pages/profile_page.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/api_values.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/initial_circle_avatar_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/part_widget.dart'; - -/// TODO : Build owner interaction with ProfileModel.owner - -class ProfilePage extends StatelessWidget { - const ProfilePage({ - Key key, - @required this.profileId, - }) : assert(profileId != null), - super(key: key); - - final String profileId; - - @override - Widget build(BuildContext context) { - logger.info('Building ProfilePage'); - - ProfileBloc _profileBloc = BlocProvider.of(context); - _profileBloc.fetchProfileDetails(profileId); - - return Scaffold( - body: StreamBuilder( - stream: _profileBloc.profileStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - List slivers = []; - slivers.add(_ProfilePageAppBar()); - - if (snapshot.hasError) { - slivers.add( - SliverToBoxAdapter( - child: - ErrorCard(message: translateError(context, snapshot.error)), - ), - ); - } else if (snapshot.hasData) { - ProfileModel profileModel = snapshot.data; - if (profileModel.type == kCVProfileType1) { - slivers.addAll([ - _ProfilePageSliver( - partId: profileModel.parts['main'], - ) - ]); - } else if (profileModel.type == kCVProfileType2) { - slivers.addAll([ - _ProfilePageSliver( - partId: profileModel.parts['header'], - ), - _ProfilePageSliver( - partId: profileModel.parts['main'], - ) - ]); - } else if (profileModel.type == kCVProfileType3) { - slivers.addAll([ - _ProfilePageSliver( - partId: profileModel.parts['side'], - ), - _ProfilePageSliver( - partId: profileModel.parts['main'], - ) - ]); - } else if (profileModel.type == kCVProfileType4) { - slivers.addAll([ - _ProfilePageSliver( - partId: profileModel.parts['header'], - ), - _ProfilePageSliver( - partId: profileModel.parts['side'], - ), - _ProfilePageSliver( - partId: profileModel.parts['main'], - ) - ]); - } else { - slivers.add( - SliverToBoxAdapter( - child: ErrorCard( - message: CVLocalizations.of(context).notSupported, - ), - ), - ); - } - } else { - slivers.add( - SliverToBoxAdapter( - child: LoadingShadowContent( - numberOfContentLines: 3, - ), - ), - ); - } - - return CustomScrollView( - slivers: slivers, - ); - }, - ), - ); - } -} - -class _ProfilePageAppBar extends StatelessWidget { - @override - Widget build(BuildContext context) { - ProfileBloc _profileBloc = BlocProvider.of(context); - - return SliverAppBar( - expandedHeight: 200, - pinned: true, - elevation: 2.0, - floating: false, - bottom: PreferredSize( - preferredSize: Size.fromHeight(6.0), - child: StreamBuilder( - stream: _profileBloc.isFetchingProfileStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { - return LinearProgressIndicator(); - } - return Container(); - }, - ), - ), - flexibleSpace: StreamBuilder( - stream: _profileBloc.profileStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - Widget titleWidget = LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 0, - ); - - Widget subtitleWidget = LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 0, - ); - - Widget avatarWidget = InitialCircleAvatar( - elevation: AppDimensions.kCVProfileAvatarElevation, - maxRadius: AppDimensions.kCVProfileAvatarMax, - minRadius: AppDimensions.kCVProfileAvatarMin, - backgroundImage: AssetImage('images/default-avatar.png'), - ); - - Widget bannerWidget = Image.asset( - 'images/default-banner.jpg', - fit: BoxFit.cover, - ); - - if (snapshot.hasData) { - ProfileModel profileModel = snapshot.data; - titleWidget = Text( - profileModel.title, - ); - subtitleWidget = Text( - profileModel.subtitle, - ); - avatarWidget = InitialCircleAvatar( - text: profileModel.title, - elevation: AppDimensions.kCVProfileAvatarElevation, - maxRadius: AppDimensions.kCVProfileAvatarMax, - minRadius: AppDimensions.kCVProfileAvatarMin, - backgroundImage: NetworkImage(profileModel.picture), - ); - - bannerWidget = Image.network( - profileModel.cover, - fit: BoxFit.cover, - ); - } - - Widget profileInfo = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - titleWidget, - subtitleWidget, - ], - ); - - Widget backgroundWidget = Stack( - children: [ - Stack( - fit: StackFit.expand, - children: [ - bannerWidget, - - ///This gradient ensures that the toolbar icons are distinct - ///against the background image. - const DecoratingBackground(), - ], - ), - Center(heightFactor: 2, child: avatarWidget), - ], - ); - return FlexibleSpaceBar( - background: backgroundWidget, - collapseMode: CollapseMode.parallax, - centerTitle: true, - title: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - profileInfo, - ], - ), - ), - ); - }, - ), - ); - } -} - -class _ProfilePageSliver extends StatelessWidget { - const _ProfilePageSliver({@required this.partId}) : assert(partId != null); - - final String partId; - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: BlocProvider( - bloc: PartBloc(), - child: PartWidget(fromId: partId), - ), - ); - } -} - -class DecoratingBackground extends StatelessWidget { - const DecoratingBackground({Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment(0.0, -1.0), - end: Alignment(0.0, 5), - colors: [Color(0x60000000), Color(0x00000000)], - ), - ), - ); - } -} diff --git a/lib/src/pages/search_page.dart b/lib/src/pages/search_page.dart deleted file mode 100644 index ae8c2b0..0000000 --- a/lib/src/pages/search_page.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/tags.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/repositories/repositories_provider.dart'; -import 'package:social_cv_client_flutter/src/widgets/profile_list_widget.dart'; - -class SearchPage extends StatelessWidget { - const SearchPage({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - RepositoriesProvider _repositories = RepositoriesProvider.of(context); - - return Scaffold( - body: CustomScrollView( - slivers: [ - SliverAppBar( - title: Text(CVLocalizations.of(context).searchTitle), - ), - SliverToBoxAdapter( - child: Hero( - tag: kHeroSearchFAB, - child: Card( - child: Container( - padding: EdgeInsets.all(10.0), - child: TextField( - onSubmitted: null, - autofocus: true, - decoration: InputDecoration( - labelText: CVLocalizations.of(context).search, - prefixIcon: Icon(Icons.search), - hintText: CVLocalizations.of(context).searchSearchBarHint, - ), - ), - ), - ), - ), - ), - SliverToBoxAdapter( - child: BlocProvider( - bloc: ProfileListBloc( - cvRepository: _repositories.cvRepository, - preferencesRepository: _repositories.preferencesRepository, - ), - child: ProfileListWidget( - fromSearch: '', - showOptions: true, - shrinkWrap: true, - physics: ClampingScrollPhysics(), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/presentation/app.dart b/lib/src/presentation/app.dart new file mode 100644 index 0000000..906b17e --- /dev/null +++ b/lib/src/presentation/app.dart @@ -0,0 +1,265 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ConfigWrapperApp extends StatefulWidget { + const ConfigWrapperApp({Key key}) : super(key: key); + + @override + State createState() => _ConfigWrapperAppState(); +} + +class _ConfigWrapperAppState extends State { + ConfigurationBloc _configBloc; + + @override + void initState() { + super.initState(); + _configBloc = ConfigurationBloc(); + + /// Inform ConfigBloc that the application have been launched + _configBloc.dispatch(AppLaunched()); + } + + @override + void dispose() { + _configBloc?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: _configBloc, + builder: (BuildContext context, ConfigurationState state) { + if (state is ConfigLoading) { + return SplashApp(); + } else if (state is ConfigLoaded) { + /// Dependency Injection of repositories + /// Use updateShouldNotify to make dependencies available in + /// `initState` methods of children widgets + return BlocProvider.value( + value: _configBloc, + child: MultiProvider( + providers: [ + Provider.value( + value: state.cvAuthService, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.authInfoRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.appPrefsRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.userRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.profileRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.partRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.groupRepository, + updateShouldNotify: (previous, current) => false, + ), + Provider.value( + value: state.entryRepository, + updateShouldNotify: (previous, current) => false, + ), + ], + child: _BlocWrapper(state: state), + ), + ); + } + return ErrorApp(error: NotImplementedYetError()); + }, + ); + } +} + +/// Wrap all global bloc to configure them +class _BlocWrapper extends StatefulWidget { + final ConfigLoaded state; + + const _BlocWrapper({Key key, @required this.state}) : super(key: key); + + @override + State createState() => _BlocWrapperState(); +} + +class _BlocWrapperState extends State<_BlocWrapper> { + final String _tag = '$_BlocWrapperState'; + + AppBloc _appBloc; + IdentityBloc _identityBloc; + LoginBloc _loginBloc; + RegisterBloc _registerBloc; + AuthenticationBloc _authBloc; + + ConfigLoaded get _state => widget.state; + + @override + void initState() { + super.initState(); + _appBloc = AppBloc( + appPreferencesRepository: _state.appPrefsRepository, + ); + + _loginBloc = LoginBloc(cvAuthService: _state.cvAuthService); + _registerBloc = RegisterBloc(cvAuthService: _state.cvAuthService); + + _authBloc = AuthenticationBloc( + authInfoRepository: _state.authInfoRepository, + cvAuthService: _state.cvAuthService, + loginBloc: _loginBloc, + registerBloc: _registerBloc, + ); + + _identityBloc = IdentityBloc( + identityRepo: _state.identityRepository, + authBloc: _authBloc, + ); + + // Inform AppBloc that the application just started + _appBloc.dispatch(AppConfigured()); + + // Inform AuthBloc that the application just started + _authBloc.dispatch(AppStarted()); + } + + @override + void dispose() { + _appBloc?.dispose(); + _loginBloc?.dispose(); + _registerBloc?.dispose(); + _identityBloc?.dispose(); + _authBloc?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:$build'); + + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: _appBloc), + BlocProvider.value(value: _authBloc), + BlocProvider.value(value: _identityBloc), + BlocProvider.value(value: _loginBloc), + BlocProvider.value(value: _registerBloc), + ], + child: _App(), + ); + } +} + +class _App extends StatelessWidget { + final String _tag = '$_App'; + + _App({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + ///Routes + final appRouter = AppRouter(); + + AppInitialized tmpState; + + return BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, AppState state) { + if (state is AppLoading || state is AppInitialized) { + if (state is AppInitialized) tmpState = state; + + return MaterialApp( + onGenerateTitle: (BuildContext context) => + CVLocalizations.of(context).appName, + theme: _buildCVTheme(tmpState.darkMode), + home: const MainPage(), + onGenerateRoute: appRouter.router.generator, + + // Use Fluro routes + localizationsDelegates: [ + const CVLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('fr'), + ], + debugShowCheckedModeBanner: false, + ); + } else if (state is AppFailure) { + return ErrorApp(error: state.error); + } + + return ErrorApp(error: NotImplementedYetError()); + }, + ); + } + + ThemeData _buildCVTheme(bool darkMode) { + ThemeData themeData; + if (!darkMode) { + themeData = ThemeData.light(); + } else { + themeData = ThemeData.dark(); + } + + themeData = themeData.copyWith( + primaryColor: AppStyles.primaryColor, + primaryColorLight: AppStyles.primaryColorLight, + primaryColorDark: AppStyles.primaryColorDark, + accentColor: AppStyles.accentColor, + inputDecorationTheme: InputDecorationTheme( + hasFloatingPlaceholder: true, + border: OutlineInputBorder(), + ), + ); + + Color buttonColor; + ButtonThemeData buttonTheme; + IconThemeData iconThemeData; + if (!darkMode) { + buttonColor = AppStyles.colorWhite; + buttonTheme = ButtonThemeData(buttonColor: themeData.primaryColorLight); + iconThemeData = IconThemeData(color: Colors.black); + } else { + buttonColor = AppStyles.primaryColorDark; + buttonTheme = ButtonThemeData(buttonColor: themeData.primaryColorDark); + iconThemeData = IconThemeData(color: Colors.white); + } + + return themeData.copyWith( + buttonColor: buttonColor, + buttonTheme: buttonTheme, + textTheme: _buildCVTextTheme(themeData.textTheme), + primaryTextTheme: _buildCVTextTheme(themeData.primaryTextTheme), + accentTextTheme: _buildCVTextTheme(themeData.accentTextTheme), + iconTheme: iconThemeData, + ); + } + + TextTheme _buildCVTextTheme(TextTheme base) { + return base.apply( + fontFamily: 'Arial', + ); + } +} diff --git a/lib/src/commons/api_values.dart b/lib/src/presentation/commons/api_values.dart similarity index 69% rename from lib/src/commons/api_values.dart rename to lib/src/presentation/commons/api_values.dart index 5997d89..331c088 100644 --- a/lib/src/commons/api_values.dart +++ b/lib/src/presentation/commons/api_values.dart @@ -1,8 +1,8 @@ ///Profile Types -const kCVProfileType1 = 'PROFILE_TYPE_MAIN'; -const kCVProfileType2 = 'PROFILE_TYPE_HEADER_MAIN'; -const kCVProfileType3 = 'PROFILE_TYPE_MAIN_SIDE'; -const kCVProfileType4 = 'PROFILE_TYPE_HEADER_MAIN_SIDE'; +const kCVProfileTypeMain = 'PROFILE_TYPE_MAIN'; +const kCVProfileTypeHeaderMain = 'PROFILE_TYPE_HEADER_MAIN'; +const kCVProfileTypeMainSide = 'PROFILE_TYPE_MAIN_SIDE'; +const kCVProfileTypeHeaderMainSide = 'PROFILE_TYPE_HEADER_MAIN_SIDE'; ///Part Types const kCVPartTypeListHorizontal = 'PART_TYPE_LIST_HORIZONTAL'; diff --git a/lib/src/presentation/commons/assets.dart b/lib/src/presentation/commons/assets.dart new file mode 100644 index 0000000..b025d74 --- /dev/null +++ b/lib/src/presentation/commons/assets.dart @@ -0,0 +1,3 @@ +class AppAssets { + static const String loginLogoImage = 'assets/images/login_logo.png'; +} diff --git a/lib/src/commons/defaults.dart b/lib/src/presentation/commons/defaults.dart similarity index 74% rename from lib/src/commons/defaults.dart rename to lib/src/presentation/commons/defaults.dart index 8682a56..8dc5c14 100644 --- a/lib/src/commons/defaults.dart +++ b/lib/src/presentation/commons/defaults.dart @@ -3,4 +3,4 @@ const String kCVItemsPerPage1 = '5'; const String kCVItemsPerPage2 = '10'; const String kCVItemsPerPage3 = '25'; const String kCVItemsPerPage4 = '50'; -const String kCVItemsPerPageDefault = kCVItemsPerPage1; +const String kCVItemsDefaultPerPage = kCVItemsPerPage1; diff --git a/lib/src/commons/paths.dart b/lib/src/presentation/commons/paths.dart similarity index 100% rename from lib/src/commons/paths.dart rename to lib/src/presentation/commons/paths.dart diff --git a/lib/src/presentation/commons/styles.dart b/lib/src/presentation/commons/styles.dart new file mode 100644 index 0000000..3d77536 --- /dev/null +++ b/lib/src/presentation/commons/styles.dart @@ -0,0 +1,115 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class AppStyles { + /// -------------------------------------------------------------------------- + /// Commons Colors + /// -------------------------------------------------------------------------- + + static const Color colorBlue = Colors.blue; + static const Color colorOrange = Colors.deepOrange; + static const Color colorPink = Colors.pink; + static const Color colorWhite = Colors.white; + static const Color colorBlack = Colors.black; + + /// -------------------------------------------------------------------------- + /// Commons Colors + /// -------------------------------------------------------------------------- + + static const Color successColor = Colors.green; + static const Color warningColor = Colors.yellow; + static const Color errorColor = Colors.red; + + /// -------------------------------------------------------------------------- + /// Basic Colors + /// -------------------------------------------------------------------------- + + static const Color primaryColor = Color(0xFF2196f3); + static const Color primaryColorLight = Color(0xFF6ec6ff); + static const Color primaryColorDark = Color(0xFF0069c0); + static const Color textOnPrimary = Color(0xFFFFFFFF); + static const Color accentColor = Color(0xFFFF5722); + static const Color accentColorLight = Color(0xFFff8a50); + static const Color accentColorDark = Color(0xFFc41c00); + static const Color textOnAccent = Color(0xFFFFFFFF); + static const Color backgroundColor = Color(0xFFFFFFFF); + static const Color backgroundColorLight = backgroundColor; + static const Color backgroundColorDark = Color(0xFF3A3A3A); + + /// -------------------------------------------------------------------------- + /// Default dimensions + /// -------------------------------------------------------------------------- + + static const double defaultCardElevation = 2.0; + static const EdgeInsets defaultCardPadding = EdgeInsets.all(15.0); + static const EdgeInsets defaultFormInputPadding = EdgeInsets.all(15.0); + + /// -------------------------------------------------------------------------- + /// Card + /// -------------------------------------------------------------------------- + + static const Color cardBackgroundColor = Color(0xFFFFFFFF); + static const Color cardBackgroundColorLight = backgroundColor; + static const Color cardBackgroundColorDark = Color(0xFF353A3A); + + static const double cardDefaultElevation = 2.0; + static const EdgeInsets cardDefaultPadding = EdgeInsets.all(20.0); + + /// Sort Dialog + + static const double sortDialogWidth = 200.0; + static const double sortDialogHeight = 300.0; + + /// -------------------------------------------------------------------------- + /// List + /// -------------------------------------------------------------------------- + + static const double listHeaderDefaultHeightMax = 40.0; + static const double listHeaderDefaultHeightMin = 40.0; + + /// -------------------------------------------------------------------------- + /// App + /// -------------------------------------------------------------------------- + + static const double appMenuButtonVerticalPadding = 3.0; + + /// -------------------------------------------------------------------------- + /// Auth + /// -------------------------------------------------------------------------- + + static const double authPageMinHeight = 800.0; + static const Color authLoginGradientEnd = primaryColorLight; + static const Color authLoginGradientStart = primaryColorDark; + + /// -------------------------------------------------------------------------- + /// Element Profile + /// -------------------------------------------------------------------------- + + static const double profileAvatarMin = 5.0; + static const double profileAvatarMax = 50.0; + static const double profileAvatarElevation = 2.0; + + /// -------------------------------------------------------------------------- + /// Element Part + /// -------------------------------------------------------------------------- + + /// -------------------------------------------------------------------------- + /// Element Group + /// -------------------------------------------------------------------------- + + static const double groupHorizontalPadding = 5.0; + static const double groupHorizontalListHeight = 300.0; + + /// -------------------------------------------------------------------------- + /// Element Entry + /// -------------------------------------------------------------------------- + + static const EdgeInsets entryPadding = EdgeInsets.all(10.0); + static const double entryTagSpacing = 4.0; + static const double entryCardElevation = 2.0; + static const double entryEventHeight = 200.0; + static const double entryEventHWidth = 300.0; + + static const double entryHorizontalListHeight = entryEventHeight; +} diff --git a/lib/src/presentation/commons/tags.dart b/lib/src/presentation/commons/tags.dart new file mode 100644 index 0000000..69b219c --- /dev/null +++ b/lib/src/presentation/commons/tags.dart @@ -0,0 +1,3 @@ +class AppHeroes { + static const String searchFab = 'TAG_HERO_SEARCH_FAB'; +} diff --git a/lib/src/presentation/localizations/cv_localization.dart b/lib/src/presentation/localizations/cv_localization.dart new file mode 100644 index 0000000..90d7067 --- /dev/null +++ b/lib/src/presentation/localizations/cv_localization.dart @@ -0,0 +1,361 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization_en.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization_fr.dart'; + +abstract class CVLocalizations { + static CVLocalizations of(BuildContext context) { + return Localizations.of(context, CVLocalizations); + } + + /// -------------------------------------------------------------------------- + /// Common + /// -------------------------------------------------------------------------- + + String get token; + + String get cancelCTA; + + String get settingsCTA; + + String get account; + + String get home; + + String get resume; + + String get profile; + + String get search; + + String get history; + + String get loadMore; + + String get retryCTA; + + String get yesCTA; + + String get noCTA; + + String get moreCTA; + + String get errorOccurred; + + /// -------------------------------------------------------------------------- + /// App + /// -------------------------------------------------------------------------- + + String get appName; + + /// -------------------------------------------------------------------------- + /// Menu Widget + /// -------------------------------------------------------------------------- + + String get menuPPCTA; + + String get menuToSCTA; + + /// -------------------------------------------------------------------------- + /// Home Page + /// -------------------------------------------------------------------------- + + String get homeTitle; + + String get homeCTA; + + String get homeWelcome; + + /// -------------------------------------------------------------------------- + /// Authentication Page - Login - SignUp + /// -------------------------------------------------------------------------- + + String get authTitle; + + String get authBubbleLoginCTA; + + String get authBubbleRegisterCTA; + + String get authNoEmailTitle; + + String get authNotEmailExplain; + + String get authNoEmailExplain; + + String get authNoPasswordTitle; + + String get authNotPasswordExplain; + + String get authNoPasswordExplain; + + String get authCreateYourAccount; + + String get authRegisterTitle; + + String get authRegisterCTA; + + String get authRegisterSucceed; + + String get authRegisterFailed; + + String get authLoginTitle; + + String get authLoginCTA; + + String get authLoginGoogleCTA; + + String get authLoginFacebookCTA; + + String get authLoginSucceed; + + String get authLoginFailed; + + String get authLogout; + + String get authLogoutCTA; + + String get authLogoutSucceed; + + String get authAccountAlreadyExistsFailure; + + String get authForgotPasswordCTA; + + String get authAlreadyHaveAccountCTA; + + String get authNoAccountCTA; + + String get authPrivacyExplain; + + String get authPrivacyReadCTA; + + String get authOr; + + /// -------------------------------------------------------------------------- + /// Account Page + /// -------------------------------------------------------------------------- + + String get accountTitle; + + String get accountCTA; + + String get accountMyProfile; + + /// -------------------------------------------------------------------------- + /// Setting Page + /// -------------------------------------------------------------------------- + + String get settingsTitle; + + String get settingsDarkModeCTA; + + String get settingsThemeLight; + + String get settingsThemeDark; + + /// Search Page + + String get searchTitle; + + String get searchSearchBarHint; + + /// -------------------------------------------------------------------------- + /// Profile Widget + /// -------------------------------------------------------------------------- + + String get profileTitle; + + String get profileWidgetDetails; + + String get profileListOptions; + + String get profileListSorting; + + String get profileListItemPerPage; + + String get profileListLoadMore; + + /// -------------------------------------------------------------------------- + /// Part Widget + /// -------------------------------------------------------------------------- + + String get partWidgetDetails; + + String get partListOptions; + + String get partListSorting; + + String get partListItemPerPage; + + String get partListLoadMore; + + /// -------------------------------------------------------------------------- + /// Group Widget + /// -------------------------------------------------------------------------- + + String get groupWidgetDetails; + + String get groupListOptions; + + String get groupListSorting; + + String get groupListItemPerPage; + + String get groupListLoadMore; + + /// -------------------------------------------------------------------------- + /// Entry Widget + /// -------------------------------------------------------------------------- + + String get entryWidgetDetails; + + String get entryListOptions; + + String get entryListSorting; + + String get entryListItemPerPage; + + String get entryListLoadMore; + + /// Sort Dialog + + String get sortDialogCancel; + + String get sortDialogConfirm; + + /// -------------------------------------------------------------------------- + /// Forms + /// -------------------------------------------------------------------------- + + String get formUsernameLabel; + + String get formEmailLabel; + + String get formEmailHint; + + String get formNoEmailExplain; + + String get formNotEmailExplain; + + String get formPasswordLabel; + + String get formNoPasswordExplain; + + String get formPassword2Label; + + String get formPasswordWrongPolicy; + + /// -------------------------------------------------------------------------- + /// Exceptions + /// -------------------------------------------------------------------------- + + String get exceptionFormatException; + + String get exceptionTimeoutException; + + /// -------------------------------------------------------------------------- + /// App Exceptions + /// -------------------------------------------------------------------------- + + String get appErrorAuthUnauthorized; + + String get appErrorAuthAccountDisabled; + + String get appErrorAuthForbidden; + + String get appErrorAuthNoToken; + + String get appErrorUserNotFound; + + String get appErrorServerSideProblem; + + /// -------------------------------------------------------------------------- + /// Errors + /// -------------------------------------------------------------------------- + + String get errorNotYetImplemented; + + String get errorNotSupported; + + /// -------------------------------------------------------------------------- + /// HTTP Client Error (4XX) + /// -------------------------------------------------------------------------- + + String get http400ClientErrorBadRequest; + + String get http401ClientErrorUnauthorized; + + String get http402ClientErrorPaymentRequired; + + String get http403ClientErrorForbidden; + + String get http404ClientErrorNotFound; + + String get http405ClientErrorMethodNotAllowed; + + String get http406ClientErrorNotAcceptable; + + String get http408ClientErrorRequestTimeout; + + String get http409ClientErrorConflict; + + String get http410ClientErrorGone; + + String get http411ClientErrorLengthRequired; + + String get http413ClientErrorPayloadTooLarge; + + String get http414ClientErrorURITooLong; + + String get http415ClientErrorUnsupportedMediaType; + + String get http417ClientErrorExpectationFailed; + + String get http426ClientErrorUpgradeRequired; + + /// -------------------------------------------------------------------------- + /// HTTP Server Error (5XX) + /// -------------------------------------------------------------------------- + + String get http500ServerErrorInternalServerError; + + String get http501ServerErrorNotImplemented; + + String get http502ServerErrorBadGateway; + + String get http503ServerErrorServiceUnavailable; + + String get http504ServerErrorGatewayTimeout; + + String get http505ServerErrorHttpVersionNotSupported; +} + +class CVLocalizationsDelegate extends LocalizationsDelegate { + const CVLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => ['en', 'fr'].contains(locale.languageCode); + + @override + Future load(Locale locale) async { + final String name = + (locale.countryCode == null || locale.countryCode.isEmpty) + ? locale.languageCode + : locale.toString(); + final String localeName = Intl.canonicalizedLocale(name); + Intl.defaultLocale = localeName; + + if (locale.languageCode == 'fr') { + return await CVLocalizationsFR.load(locale); + } else { + return await CVLocalizationsEN.load(locale); + } + } + + @override + bool shouldReload(CVLocalizationsDelegate old) => false; + + @override + String toString() => 'DefaultCVLocalizations.delegate(en_US)'; +} diff --git a/lib/src/presentation/localizations/cv_localization_en.dart b/lib/src/presentation/localizations/cv_localization_en.dart new file mode 100644 index 0000000..d28288d --- /dev/null +++ b/lib/src/presentation/localizations/cv_localization_en.dart @@ -0,0 +1,479 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; + +class CVLocalizationsEN implements CVLocalizations { + const CVLocalizationsEN(); + + /// -------------------------------------------------------------------------- + /// Common + /// -------------------------------------------------------------------------- + + @override + String get token => 'Token'; + + @override + String get cancelCTA => 'Cancel'; + + @override + String get settingsCTA => 'Settings'; + + @override + String get account => 'Account'; + + @override + String get home => 'Home'; + + @override + String get resume => 'Resume'; + + @override + String get profile => 'Profile'; + + @override + String get search => 'Search'; + + @override + String get history => 'History'; + + @override + String get loadMore => 'Load more'; + + @override + String get retryCTA => 'Retry'; + + @override + String get yesCTA => 'Yes'; + + @override + String get noCTA => 'No'; + + @override + String get moreCTA => 'More'; + + @override + String get errorOccurred => 'An error occurred'; + + /// -------------------------------------------------------------------------- + /// App + /// -------------------------------------------------------------------------- + + @override + String get appName => 'Social CV'; + + /// -------------------------------------------------------------------------- + /// Menu Widget + /// -------------------------------------------------------------------------- + + @override + String get menuPPCTA => 'Privacy Policy'; + + @override + String get menuToSCTA => 'Terms of Service'; + + /// -------------------------------------------------------------------------- + /// Home Page + /// -------------------------------------------------------------------------- + + @override + String get homeTitle => 'Social CV'; + + @override + String get homeCTA => 'Home'; + + @override + String get homeWelcome => 'Welcome on our new resume social network !'; + + /// -------------------------------------------------------------------------- + /// Authentication Page - Login - SignUp + /// -------------------------------------------------------------------------- + + @override + String get authTitle => 'Connection'; + + @override + String get authBubbleLoginCTA => 'Login'; + + @override + String get authBubbleRegisterCTA => 'Register'; + + @override + String get authNoEmailTitle => 'Empty email'; + + @override + String get authNotEmailExplain => 'Please enter a real e-mail.'; + + @override + String get authNoEmailExplain => 'Please provide an email'; + + @override + String get authNoPasswordTitle => 'Empty password'; + + @override + String get authNoPasswordExplain => 'Please provide a password'; + + @override + String get authCreateYourAccount => 'Create your account'; + + @override + String get authPrivacyExplain => + 'Like privacy? We feel you. We don’t use or sell your data.'; + + @override + String get authPrivacyReadCTA => 'Touch to read our privacy policy.'; + + @override + String get authAlreadyHaveAccountCTA => 'Already have an account? Sign-in'; + + @override + String get authNotPasswordExplain => 'This is not a password'; + + @override + String get authRegisterTitle => 'Register'; + + @override + String get authRegisterCTA => 'Sign-up'; + + @override + String get authRegisterFailed => 'Registration failed!'; + + @override + String get authRegisterSucceed => 'Registration succeed!'; + + @override + String get authLoginTitle => 'Sign-in'; + + @override + String get authLoginCTA => 'Sign-in'; + + @override + String get authLoginGoogleCTA => 'Sign-in with Google'; + + @override + String get authLoginFacebookCTA => 'Sign-in with Facebook'; + + @override + String get authLoginSucceed => 'Login succeed!'; + + @override + String get authLoginFailed => 'Login failed!'; + + @override + String get authLogout => 'Logout'; + + @override + String get authLogoutCTA => 'Logout'; + + @override + String get authAccountAlreadyExistsFailure => + 'An account with this username or email already exists.'; + + @override + String get authLogoutSucceed => 'Logout succeed!'; + + @override + String get authForgotPasswordCTA => 'Forgot password?'; + + @override + String get authNoAccountCTA => 'Don\'t have an account yet? Sign-up'; + + @override + String get authOr => 'OR'; + + /// -------------------------------------------------------------------------- + /// Account Page + /// -------------------------------------------------------------------------- + + @override + String get accountTitle => 'Account'; + + @override + String get accountCTA => 'Account'; + + @override + String get accountMyProfile => 'My profiles'; + + /// -------------------------------------------------------------------------- + /// Setting Page + /// -------------------------------------------------------------------------- + + @override + String get settingsTitle => 'Settings'; + + @override + String get settingsDarkModeCTA => 'Dark Mode'; + + @override + String get settingsThemeLight => 'Light'; + + @override + String get settingsThemeDark => 'Dark'; + + /// Search Page + + @override + String get searchTitle => 'Search'; + + @override + String get searchSearchBarHint => 'Search resume...'; + + /// -------------------------------------------------------------------------- + /// Profile Widget + /// -------------------------------------------------------------------------- + + @override + String get profileTitle => 'Profile'; + + @override + String get profileWidgetDetails => 'Profile details'; + + @override + String get profileListOptions => 'Profile options'; + + @override + String get profileListSorting => 'Sorting Profiles'; + + @override + String get profileListItemPerPage => 'Profile per page'; + + @override + String get profileListLoadMore => 'Load more profiles'; + + /// -------------------------------------------------------------------------- + /// Part Widget + /// -------------------------------------------------------------------------- + + @override + String get partWidgetDetails => 'Part détails'; + + @override + String get partListOptions => 'Part list options'; + + @override + String get partListSorting => 'Sorting parts'; + + @override + String get partListItemPerPage => 'Parts per page'; + + @override + String get partListLoadMore => 'Load more parts'; + + /// -------------------------------------------------------------------------- + /// Group Widget + /// -------------------------------------------------------------------------- + + @override + String get groupWidgetDetails => 'Group détails'; + + @override + String get groupListOptions => 'Group list options'; + + @override + String get groupListSorting => 'Sorting groups'; + + @override + String get groupListItemPerPage => 'Groups per page'; + + @override + String get groupListLoadMore => 'Load more groups'; + + /// -------------------------------------------------------------------------- + /// Entry Widget + /// -------------------------------------------------------------------------- + + @override + String get entryWidgetDetails => 'Entry détails'; + + @override + String get entryListOptions => 'Entry list options'; + + @override + String get entryListSorting => 'Sorting entries'; + + @override + String get entryListItemPerPage => 'Entries per page'; + + @override + String get entryListLoadMore => 'Load more entries'; + + /// Sort Dialog + @override + String get sortDialogCancel => 'Cancel'; + + @override + String get sortDialogConfirm => 'Confirm'; + + /// -------------------------------------------------------------------------- + /// Forms + /// -------------------------------------------------------------------------- + + @override + String get formUsernameLabel => 'Username'; + + @override + String get formEmailLabel => 'Email'; + + @override + String get formEmailHint => 'someone@email.com'; + + @override + String get formNotEmailExplain => 'Please enter a real e-mail.'; + + @override + String get formNoEmailExplain => 'Please provide an email'; + + @override + String get formPasswordLabel => 'Password'; + + @override + String get formNoPasswordExplain => 'Please provide a password'; + + @override + String get formPassword2Label => 'Repeat password'; + + @override + String get formPasswordWrongPolicy => + 'The password do not fit to our password policy.'; + + /// -------------------------------------------------------------------------- + /// App Exceptions + /// -------------------------------------------------------------------------- + + @override + String get appErrorUserNotFound => 'User not found'; + + @override + String get appErrorAuthForbidden => 'Access forbidden'; + + @override + String get appErrorAuthNoToken => 'No token emitted'; + + @override + String get appErrorAuthUnauthorized => 'Not authorized'; + + @override + String get appErrorAuthAccountDisabled => 'Account disabled'; + + @override + String get appErrorServerSideProblem => 'A server side error occured'; + + /// -------------------------------------------------------------------------- + /// App Errors + /// -------------------------------------------------------------------------- + + @override + String get errorNotYetImplemented => 'Not yet implemented'; + + @override + String get errorNotSupported => 'Not supported'; + + /// -------------------------------------------------------------------------- + /// Others Exceptions + /// -------------------------------------------------------------------------- + + @override + String get exceptionFormatException => 'Exception : Wrong Format'; + + @override + String get exceptionTimeoutException => 'Exception : Request Timeout'; + + /// -------------------------------------------------------------------------- + /// HTTP Client Error (4XX) + /// -------------------------------------------------------------------------- + + @override + String get http400ClientErrorBadRequest => 'Bad request'; + + @override + String get http401ClientErrorUnauthorized => 'Unauthorized'; + + @override + String get http402ClientErrorPaymentRequired => 'Payment required'; + + @override + String get http403ClientErrorForbidden => 'Forbidden'; + + @override + String get http404ClientErrorNotFound => 'Not found'; + + @override + String get http405ClientErrorMethodNotAllowed => 'Not allowed'; + + @override + String get http406ClientErrorNotAcceptable => 'Not acceptable'; + + @override + String get http408ClientErrorRequestTimeout => 'Request timeout'; + + @override + String get http409ClientErrorConflict => 'Conflict'; + + @override + String get http410ClientErrorGone => 'Gone'; + + @override + String get http411ClientErrorLengthRequired => 'Length required'; + + @override + String get http413ClientErrorPayloadTooLarge => 'Payload too large'; + + @override + String get http414ClientErrorURITooLong => 'URI too long'; + + @override + String get http415ClientErrorUnsupportedMediaType => 'Unsupported media type'; + + @override + String get http417ClientErrorExpectationFailed => 'Expectation Failed'; + + @override + String get http426ClientErrorUpgradeRequired => 'Upgrade required'; + + /// -------------------------------------------------------------------------- + /// HTTP Server Error (5XX) + /// -------------------------------------------------------------------------- + + @override + String get http500ServerErrorInternalServerError => 'Internal Server Error'; + + @override + String get http501ServerErrorNotImplemented => 'Not implemented'; + + @override + String get http502ServerErrorBadGateway => 'Bad Gateway'; + + @override + String get http503ServerErrorServiceUnavailable => 'Service Unavailable'; + + @override + String get http504ServerErrorGatewayTimeout => 'Gateway Timeout'; + + @override + String get http505ServerErrorHttpVersionNotSupported => + 'HTTP Version Not Supported'; + + /// -------------------------------------------------------------------------- + /// Misc + /// -------------------------------------------------------------------------- + + /// Creates an object that provides US English resource values for the + /// application. + /// + /// The [locale] parameter is ignored. + /// + /// This method is typically used to create a [LocalizationsDelegate]. + /// The [MaterialApp] does so by default. + static FutureOr load(Locale locale) { + return SynchronousFuture(const CVLocalizationsEN()); + } + + /// A [LocalizationsDelegate] that uses [CVLocalizationsEN.load] + /// to create an instance of this class. + /// + /// [MaterialApp] automatically adds this value to [MaterialApp.localizationsDelegates]. + static const LocalizationsDelegate delegate = + CVLocalizationsDelegate(); +} diff --git a/lib/src/presentation/localizations/cv_localization_fr.dart b/lib/src/presentation/localizations/cv_localization_fr.dart new file mode 100644 index 0000000..0bceaf2 --- /dev/null +++ b/lib/src/presentation/localizations/cv_localization_fr.dart @@ -0,0 +1,482 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; + +class CVLocalizationsFR implements CVLocalizations { + const CVLocalizationsFR(); + + /// -------------------------------------------------------------------------- + /// Common + /// -------------------------------------------------------------------------- + + @override + String get token => 'Jeton'; + + @override + String get cancelCTA => 'Annuler'; + + @override + String get account => 'Compte'; + + @override + String get home => 'Accueil'; + + @override + String get resume => 'CV'; + + @override + String get profile => 'Profil'; + + @override + String get search => 'Rechercher'; + + @override + String get history => 'Historique'; + + @override + String get loadMore => 'Charger plus'; + + @override + String get retryCTA => 'Re-éssayer'; + + @override + String get yesCTA => 'Oui'; + + @override + String get noCTA => 'Non'; + + @override + String get authLoginSucceed => 'Connecté'; + + @override + String get moreCTA => 'Plus'; + + @override + String get errorOccurred => 'Une erreur s\'est produite.'; + + /// -------------------------------------------------------------------------- + /// App + /// -------------------------------------------------------------------------- + + @override + String get appName => 'Social CV'; + + /// -------------------------------------------------------------------------- + /// Menu Widget + /// -------------------------------------------------------------------------- + + @override + String get menuPPCTA => 'Politique de confidentialité'; + + @override + String get menuToSCTA => 'Termes de Service'; + + /// -------------------------------------------------------------------------- + /// Home Page + /// -------------------------------------------------------------------------- + + @override + String get homeTitle => 'Social CV'; + + @override + String get homeCTA => 'Accueil'; + + @override + String get homeWelcome => 'Bienvenue sur notre nouveau réseau social de CV !'; + + /// -------------------------------------------------------------------------- + /// Authentication Page - Login - SignUp + /// -------------------------------------------------------------------------- + + @override + String get authTitle => 'Connexion'; + + @override + String get authBubbleLoginCTA => 'Login'; + + @override + String get authBubbleRegisterCTA => 'Register'; + + @override + String get authNoEmailTitle => 'E-mail vide'; + + @override + String get authNotEmailExplain => 'Merci de renseigner un e-mail existant.'; + + @override + String get authNoEmailExplain => 'Merci de renseigner un e-mail'; + + @override + String get authNoPasswordTitle => 'Mot de passe vide'; + + @override + String get authNoPasswordExplain => 'Merci de renseigner un mot de passe'; + + @override + String get authCreateYourAccount => 'Créez votre compte'; + + @override + String get authRegisterTitle => 'Inscription'; + + @override + String get authRegisterCTA => 'S\'inscrire'; + + @override + String get authRegisterFailed => 'Inscription échoué !'; + + @override + String get authRegisterSucceed => 'Inscription réussite !'; + + @override + String get authLoginTitle => 'Connectez vous avec votre compte'; + + @override + String get authLoginCTA => 'Se connecter'; + + @override + String get authLoginGoogleCTA => 'Se connecter avec Google'; + + @override + String get authLoginFacebookCTA => 'Se connecter avec Facebook'; + + @override + String get authLoginFailed => 'Connexion échoué !'; + + @override + String get authLogout => 'Se déconnecter'; + + @override + String get authLogoutCTA => 'Se déconnecter'; + + @override + String get authAccountAlreadyExistsFailure => + 'Un compte avec le même nom d\'utilisateur ou e-mail existe déjà.'; + + @override + String get authLogoutSucceed => 'Déconnexion réussite !'; + + @override + String get authPrivacyExplain => + 'Vous aimez votre vie privée ? Nous le savons. Nous n\'utilisons, ni ' + 'vendons vos données.'; + + @override + String get authPrivacyReadCTA => + 'Touchez ici pour lire notre politique de confidentialité.'; + + @override + String get authAlreadyHaveAccountCTA => + 'Vous avez déjà un compte ? Connectez-vous'; + + @override + String get authForgotPasswordCTA => 'Mot de passe oublié ?'; + + @override + String get authNoAccountCTA => 'Vous n\'avez pas de compte ? Inscrivez-vous'; + + @override + String get authNotPasswordExplain => 'Ceci n\'est pas un mot de passe'; + + @override + String get authOr => 'OU'; + + /// -------------------------------------------------------------------------- + /// Account Page + /// -------------------------------------------------------------------------- + + @override + String get accountTitle => 'Compte'; + + @override + String get accountCTA => 'Compte'; + + @override + String get accountMyProfile => 'Mes profils'; + + /// Settings Pages + + @override + String get settingsTitle => 'Paramètres'; + + @override + String get settingsCTA => 'Paramètres'; + + @override + String get settingsDarkModeCTA => 'Mode Sombre'; + + @override + String get settingsThemeLight => 'Claire'; + + @override + String get settingsThemeDark => 'Sombre'; + + /// Search Page + + @override + String get searchTitle => 'Recherche'; + + @override + String get searchSearchBarHint => 'Rechercher un profil ...'; + + /// -------------------------------------------------------------------------- + /// Profile Widget + /// -------------------------------------------------------------------------- + + @override + String get profileTitle => 'Profil'; + + @override + String get profileWidgetDetails => 'Détails du profile'; + + @override + String get profileListOptions => 'Options profiles'; + + @override + String get profileListSorting => 'Trier Profiles'; + + @override + String get profileListItemPerPage => 'Profils par page'; + + @override + String get profileListLoadMore => 'Charger plus de profiles'; + + /// -------------------------------------------------------------------------- + /// Part Widget + /// -------------------------------------------------------------------------- + + @override + String get partWidgetDetails => 'Détails de la partie'; + + @override + String get partListOptions => 'Options'; + + @override + String get partListSorting => 'Trier les parties'; + + @override + String get partListItemPerPage => 'Parties par page'; + + @override + String get partListLoadMore => 'charger plus de parties'; + + /// -------------------------------------------------------------------------- + /// Group Widget + /// -------------------------------------------------------------------------- + + @override + String get groupWidgetDetails => 'Détails du groupe'; + + @override + String get groupListOptions => 'Options'; + + @override + String get groupListSorting => 'Trier les groupes'; + + @override + String get groupListItemPerPage => 'Groupes par page'; + + @override + String get groupListLoadMore => 'charger plus de groupes'; + + /// -------------------------------------------------------------------------- + /// Entry Widget + /// -------------------------------------------------------------------------- + + @override + String get entryWidgetDetails => 'Détails de l\'entrée'; + + @override + String get entryListOptions => 'Options'; + + @override + String get entryListSorting => 'Trier les entrées'; + + @override + String get entryListItemPerPage => 'Entrées par page'; + + @override + String get entryListLoadMore => 'Charger plus de entrées'; + + /// Sort Dialog + + @override + String get sortDialogCancel => 'Annuler'; + + @override + String get sortDialogConfirm => 'Valider'; + + /// -------------------------------------------------------------------------- + /// Forms + /// -------------------------------------------------------------------------- + + @override + String get formUsernameLabel => 'Nom d\'utilisateur'; + + @override + String get formEmailLabel => 'Email'; + + @override + String get formEmailHint => 'someone@email.com'; + + @override + String get formNotEmailExplain => 'Please enter a real e-mail.'; + + @override + String get formNoEmailExplain => 'Please provide an email'; + + @override + String get formPasswordLabel => 'Password'; + + @override + String get formNoPasswordExplain => 'Please provide a password'; + + @override + String get formPassword2Label => 'Repeat password'; + + @override + String get formPasswordWrongPolicy => + 'The password do not fit to our password policy.'; + + /// -------------------------------------------------------------------------- + /// App Exceptions + /// -------------------------------------------------------------------------- + + @override + String get appErrorUserNotFound => 'User not found'; + + @override + String get appErrorAuthForbidden => 'Access forbidden'; + + @override + String get appErrorAuthNoToken => 'No token emitted'; + + @override + String get appErrorAuthUnauthorized => 'Not authorized'; + + @override + String get appErrorAuthAccountDisabled => 'Account disabled'; + + @override + String get appErrorServerSideProblem => 'A server side error occured'; + + /// -------------------------------------------------------------------------- + /// App Errors + /// -------------------------------------------------------------------------- + + @override + String get errorNotYetImplemented => 'Pas encore implémenté'; + + @override + String get errorNotSupported => 'Non supporté'; + + /// -------------------------------------------------------------------------- + /// Others Exceptions + /// -------------------------------------------------------------------------- + + @override + String get exceptionFormatException => 'Exception : Mauvais Format'; + + @override + String get exceptionTimeoutException => 'Exception : Requete Expiré'; + + /// -------------------------------------------------------------------------- + /// HTTP Client Error (4XX) + /// -------------------------------------------------------------------------- + + @override + String get http400ClientErrorBadRequest => 'Mauvaise requete'; + + @override + String get http401ClientErrorUnauthorized => 'Non authorisé'; + + @override + String get http402ClientErrorPaymentRequired => 'Paiement requis'; + + @override + String get http403ClientErrorForbidden => 'Interdit'; + + @override + String get http404ClientErrorNotFound => 'Introuvable'; + + @override + String get http405ClientErrorMethodNotAllowed => 'Non autorisé'; + + @override + String get http406ClientErrorNotAcceptable => 'Non acceptable'; + + @override + String get http408ClientErrorRequestTimeout => 'Requete expirée'; + + @override + String get http409ClientErrorConflict => 'Conflit'; + + @override + String get http410ClientErrorGone => 'Disparu'; + + @override + String get http411ClientErrorLengthRequired => 'Taille requise'; + + @override + String get http413ClientErrorPayloadTooLarge => 'Payload trop large'; + + @override + String get http414ClientErrorURITooLong => 'Lien trop long'; + + @override + String get http415ClientErrorUnsupportedMediaType => + 'Type de média non supporté'; + + @override + String get http417ClientErrorExpectationFailed => 'Échoué'; + + @override + String get http426ClientErrorUpgradeRequired => 'Mise à jour requise'; + + /// -------------------------------------------------------------------------- + /// HTTP Server Error (5XX) + /// -------------------------------------------------------------------------- + + @override + String get http500ServerErrorInternalServerError => 'Erreur Serveur interne'; + + @override + String get http501ServerErrorNotImplemented => 'Non implementé'; + + @override + String get http502ServerErrorBadGateway => 'Mauvaise passerelle'; + + @override + String get http503ServerErrorServiceUnavailable => 'Service non disponible '; + + @override + String get http504ServerErrorGatewayTimeout => 'Temps ecoulé'; + + @override + String get http505ServerErrorHttpVersionNotSupported => + 'Version HTTP non supporté'; + + /// -------------------------------------------------------------------------- + /// Misc + /// -------------------------------------------------------------------------- + + /// Creates an object that provides US English resource values for the + /// application. + /// + /// The [locale] parameter is ignored. + /// + /// This method is typically used to create a [LocalizationsDelegate]. + /// The [MaterialApp] does so by default. + static FutureOr load(Locale locale) { + return SynchronousFuture(const CVLocalizationsFR()); + } + + /// A [LocalizationsDelegate] that uses [CVLocalizationsFR.load] + /// to create an instance of this class. + /// + /// [MaterialApp] automatically adds this value to [MaterialApp.localizationsDelegates]. + static const LocalizationsDelegate delegate = + CVLocalizationsDelegate(); +} diff --git a/lib/src/presentation/mappers/model_mapper.dart b/lib/src/presentation/mappers/model_mapper.dart new file mode 100644 index 0000000..a534d36 --- /dev/null +++ b/lib/src/presentation/mappers/model_mapper.dart @@ -0,0 +1,117 @@ +import 'package:social_cv_client_flutter/data.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/data/models/envelop_models.dart'; + +/// [ModelMapper] for MVVM pattern +class ModelMapper { + final String _tag = '$ModelMapper'; + + static ModelMapper _instance; + + static _initState() { + _instance = ModelMapper(); + } + + static ModelMapper get instance { + if (_instance == null) { + _initState(); + } + return _instance; + } + + /// [ResponseAuthDataModel] to [AccessTokenViewModelModel] + AccessTokenViewModelModel toAccessTokenViewModelModel( + ResponseAuthDataModel dataModel) { + return AccessTokenViewModelModel( + accessToken: dataModel.accessToken, + refreshToken: dataModel.refreshToken, + accessTokenExpiresAt: dataModel.accessTokenExpiresIn, + tokenType: dataModel.tokenType, + ); + } + + /// [UserDataModel] to [UserViewModel] + UserViewModel toUserViewModel(UserDataModel dataModel) { + return UserViewModel( + id: dataModel.id, + disabled: dataModel.disabled, + username: dataModel.username, + email: dataModel.email, + picture: dataModel.picture, + profileIds: dataModel.profileIds, + permission: dataModel.permission, + createdAt: dataModel.createdAt, + updatedAt: dataModel.updatedAt, + version: dataModel.version, + ); + } + + /// [ProfileDataModel] to [ProfileViewModel] + ProfileViewModel toProfileViewModel(ProfileDataModel dataModel) { + /// TODO: Map Profile type with enum + return ProfileViewModel( + id: dataModel.id, + title: dataModel.title, + subtitle: dataModel.subtitle, + picture: dataModel.picture, + cover: dataModel.cover, + partIds: dataModel.partIds, + type: dataModel.type, + ownerId: dataModel.ownerId, + createdAt: dataModel.createdAt, + updatedAt: dataModel.updatedAt, + version: dataModel.version, + ); + } + + /// [PartDataModel] to [PartViewModel] + PartViewModel toPartViewModel(PartDataModel dataModel) { + /// TODO: Map Part type with enum + return PartViewModel( + id: dataModel.id, + name: dataModel.name, + groupIds: dataModel.groupIds, + type: dataModel.type, + ownerId: dataModel.ownerId, + createdAt: dataModel.createdAt, + updatedAt: dataModel.updatedAt, + version: dataModel.version, + ); + } + + /// [GroupDataModel] to [GroupViewModel] + GroupViewModel toGroupViewModel(GroupDataModel dataModel) { + /// TODO: Map Group type with enum + return GroupViewModel( + id: dataModel.id, + name: dataModel.name, + entryIds: dataModel.entryIds, + type: dataModel.type, + ownerId: dataModel.ownerId, + createdAt: dataModel.createdAt, + updatedAt: dataModel.updatedAt, + version: dataModel.version, + ); + } + + /// [EntryDataModel] to [EntryViewModel] + EntryViewModel toEntryViewModel(EntryDataModel dataModel) { + /// TODO: Map Entry type with enum + return EntryViewModel( + id: dataModel.id, + name: dataModel.name, + content: dataModel.content, + startDate: dataModel.startDate, + endDate: dataModel.endDate, + location: dataModel.location, + type: dataModel.type, + ownerId: dataModel.ownerId, + createdAt: dataModel.createdAt, + updatedAt: dataModel.updatedAt, + version: dataModel.version, + ); + } + + @override + String toString() => '$runtimeType{}'; +} diff --git a/lib/src/presentation/models/api_models.dart b/lib/src/presentation/models/api_models.dart new file mode 100644 index 0000000..5a2376b --- /dev/null +++ b/lib/src/presentation/models/api_models.dart @@ -0,0 +1,21 @@ +class AccessTokenViewModelModel { + final String accessToken; + final String refreshToken; + final int accessTokenExpiresAt; + final String tokenType; + + AccessTokenViewModelModel({ + this.accessToken, + this.refreshToken, + this.accessTokenExpiresAt, + this.tokenType, + }) : super(); + + @override + String toString() => '$runtimeType{ ' + 'accessToken: $accessToken, ' + 'refreshToken: $refreshToken, ' + 'accessTokenExpiresAt: $accessTokenExpiresAt, ' + 'tokenType: $tokenType' + ' }'; +} diff --git a/lib/src/presentation/models/cursor_model.dart b/lib/src/presentation/models/cursor_model.dart new file mode 100644 index 0000000..9ecf660 --- /dev/null +++ b/lib/src/presentation/models/cursor_model.dart @@ -0,0 +1,24 @@ +class Cursor { + /// [offset] indicate the offset of the search + final int offset; + + /// [limit] indicate the elements limits of a result + final int limit; + + const Cursor({this.offset = 0, this.limit = 5}) + : assert(offset != null, 'No offset($int) given'), + assert(limit != null, 'No limit($int) given'); + + @override + String toString() => '$runtimeType{ ' + 'offset: $offset, ' + 'limit: $limit' + ' }'; + + Cursor copyWith({int offset, int limit}) { + return Cursor( + offset: offset ?? this.offset, + limit: limit ?? this.limit, + ); + } +} diff --git a/lib/src/presentation/models/element_model.dart b/lib/src/presentation/models/element_model.dart new file mode 100644 index 0000000..64d9523 --- /dev/null +++ b/lib/src/presentation/models/element_model.dart @@ -0,0 +1,23 @@ +import 'package:meta/meta.dart'; + +abstract class ElementViewModel { + String id; + DateTime createdAt; + DateTime updatedAt; + int version; + + ElementViewModel({ + @required this.id, + this.createdAt, + this.updatedAt, + this.version, + }) : super(); + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/presentation/models/entry_model.dart b/lib/src/presentation/models/entry_model.dart new file mode 100644 index 0000000..81c2458 --- /dev/null +++ b/lib/src/presentation/models/entry_model.dart @@ -0,0 +1,74 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +class EntryViewModel extends ElementViewModel { + String name; + String type; + dynamic content; + String startDate; + String endDate; + String location; + String ownerId; + + EntryViewModel({ + String id, + this.name, + this.type, + this.content, + this.startDate, + this.endDate, + this.location, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + EntryViewModel copyWith({ + String id, + String name, + String type, + String content, + String startDate, + String endDate, + String location, + String groupId, + String ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) { + return EntryViewModel( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + location: location ?? this.location, + ownerId: ownerId ?? this.ownerId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + version: version ?? this.version, + ); + } + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'type: $type, ' + 'content: $content, ' + 'startDate: $startDate, ' + 'endDate: $endDate, ' + 'location: $location, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/presentation/models/group_model.dart b/lib/src/presentation/models/group_model.dart new file mode 100644 index 0000000..2081daa --- /dev/null +++ b/lib/src/presentation/models/group_model.dart @@ -0,0 +1,58 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +class GroupViewModel extends ElementViewModel { + String name; + List entryIds; + String type; + String ownerId; + + GroupViewModel({ + String id, + this.name, + this.entryIds, + this.type, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + GroupViewModel copyWith({ + String id, + String name, + List entryIds, + String type, + String ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) { + return GroupViewModel( + id: id ?? this.id, + name: name ?? this.name, + entryIds: entryIds ?? this.entryIds, + type: type ?? this.type, + ownerId: ownerId ?? this.ownerId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + version: version ?? this.version, + ); + } + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'type: $type, ' + 'entryIds: $entryIds, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/presentation/models/part_model.dart b/lib/src/presentation/models/part_model.dart new file mode 100644 index 0000000..afe14e6 --- /dev/null +++ b/lib/src/presentation/models/part_model.dart @@ -0,0 +1,58 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +class PartViewModel extends ElementViewModel { + String name; + List groupIds; + String type; + String ownerId; + + PartViewModel({ + String id, + this.name, + this.groupIds, + this.type, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + PartViewModel copyWith({ + String id, + String name, + List groupIds, + String type, + String ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) { + return PartViewModel( + id: id ?? this.id, + name: name ?? this.name, + groupIds: groupIds ?? this.groupIds, + type: type ?? this.type, + ownerId: ownerId ?? this.ownerId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + version: version ?? this.version, + ); + } + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'name: $name, ' + 'groupIds: $groupIds, ' + 'type: $type, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/presentation/models/profile_model.dart b/lib/src/presentation/models/profile_model.dart new file mode 100644 index 0000000..72fde1a --- /dev/null +++ b/lib/src/presentation/models/profile_model.dart @@ -0,0 +1,73 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +class ProfileViewModel extends ElementViewModel { + String title; + String subtitle; + Uri picture; + Uri cover; + String type; + List partIds; + String ownerId; + + ProfileViewModel({ + String id, + this.title, + this.subtitle, + this.picture, + this.cover, + this.partIds, + this.type, + this.ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + ProfileViewModel copyWith({ + String id, + String title, + String subtitle, + Uri picture, + Uri cover, + List partIds, + String type, + String ownerId, + DateTime createdAt, + DateTime updatedAt, + int version, + }) { + return ProfileViewModel( + id: id ?? this.id, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + picture: picture ?? this.picture, + cover: cover ?? this.cover, + type: type ?? this.type, + partIds: partIds ?? this.partIds, + ownerId: ownerId ?? this.ownerId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + version: version ?? this.version, + ); + } + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'title: $title, ' + 'subtitle: $subtitle, ' + 'picture: $picture, ' + 'cover: $cover, ' + 'type: $type, ' + 'partIds: $partIds, ' + 'owner: $ownerId, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'verrsion: $version' + ' }'; +} diff --git a/lib/src/presentation/models/user_model.dart b/lib/src/presentation/models/user_model.dart new file mode 100644 index 0000000..8142760 --- /dev/null +++ b/lib/src/presentation/models/user_model.dart @@ -0,0 +1,42 @@ +import 'package:social_cv_client_flutter/presentation.dart'; + +class UserViewModel extends ElementViewModel { + bool disabled; + String email; + String username; + String picture; + List profileIds; + dynamic permission; + + UserViewModel({ + String id, + this.disabled, + this.email, + this.username, + this.picture, + this.profileIds, + this.permission, + DateTime createdAt, + DateTime updatedAt, + int version, + }) : super( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + version: version, + ); + + @override + String toString() => '$runtimeType{ ' + 'id: $id, ' + 'disabled: $disabled, ' + 'email: $email, ' + 'username: $username, ' + 'picture: $picture, ' + 'profileIds: $profileIds, ' + 'permission: $permission, ' + 'createdAt: $createdAt, ' + 'updatedAt: $updatedAt, ' + 'version: $version' + ' }'; +} diff --git a/lib/src/presentation/models/view_models.dart b/lib/src/presentation/models/view_models.dart new file mode 100644 index 0000000..dbca130 --- /dev/null +++ b/lib/src/presentation/models/view_models.dart @@ -0,0 +1,2 @@ +/// This is a placeholder +/// Most of needed view models are in the common library diff --git a/lib/src/presentation/pages/account_page.dart b/lib/src/presentation/pages/account_page.dart new file mode 100644 index 0000000..f66629e --- /dev/null +++ b/lib/src/presentation/pages/account_page.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/logger.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/navigation.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/error_widget.dart'; + +class AccountPage extends StatelessWidget { + final String _tag = '$AccountPage'; + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return SafeArea( + left: false, + right: false, + child: BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, AuthenticationState state) { + if (state is AuthenticationUninitialized) + return Container(); + else if (state is AuthenticationUnauthenticated) + return _AccountPageDetailsNotConnected(); + else if (state is AuthenticationAuthenticated) + return _AccountPageDetailsConnected(); + else if (state is AuthenticationLoading) + return Center(child: CircularProgressIndicator()); + }, + ), + ); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// // +// _AccountPageDetailsNotConnected // +// // +//////////////////////////////////////////////////////////////////////////////// +class _AccountPageDetailsNotConnected extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: RaisedButton( + child: Text(CVLocalizations.of(context).authLoginCTA), + onPressed: () => navigateToLogin(context), + ), + ); + } +} + +/// ---------------------------------------------------------------------------- +/// _AccountPageDetailsConnected +/// ---------------------------------------------------------------------------- + +class _AccountPageDetailsConnected extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, IdentityState state) { + if (state is IdentityUninitialized) { + } else if (state is IdentityLoaded) { + return ListView( + children: [ + ExpansionTile( + leading: Icon(MdiIcons.accountBoxMultiple), + title: Text(CVLocalizations.of(context).accountMyProfile), + children: [ +// BlocProvider( +// bloc: ElementListBloc( +// cvRepository: _repositories.cvRepository, +// preferencesRepository: +// _repositories.preferencesRepository, +// ), +// child: ProfileListWidget( +// fromUserEntity: snapshot.data, +// showOptions: false, +// shrinkWrap: true, +// physics: ClampingScrollPhysics(), +// ), +// ), + ], + ), + ], + ); + } else if (state is IdentityFailed) { + return ErrorCard(error: state.error); + } + return ErrorCard(error: NotImplementedYetError()); + }, + ); + } +} diff --git a/lib/src/presentation/pages/auth_page.dart b/lib/src/presentation/pages/auth_page.dart new file mode 100644 index 0000000..7fc7c24 --- /dev/null +++ b/lib/src/presentation/pages/auth_page.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class AuthPage extends StatefulWidget { + @override + _AuthPageState createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + final String _tag = '$_AuthPageState'; + + // Variable + double screenWidth; + double screenHeight; + + PageController _pageController; + + Color left = Colors.black; + Color right = Colors.white; + + // Business + @override + void initState() { + print('$_tag:initState()'); + super.initState(); + _pageController = PageController(); + } + + @override + void dispose() { + print('$_tag:dispose()'); + _pageController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + print('$_tag:build'); + screenHeight = MediaQuery.of(context).size.height; + screenWidth = MediaQuery.of(context).size.width; + + screenHeight = (screenHeight > AppStyles.authPageMinHeight) + ? screenHeight + : AppStyles.authPageMinHeight; + + return Scaffold( + backgroundColor: AppStyles.primaryColor, + body: MultiBlocListener( + listeners: [ + BlocListener( + bloc: BlocProvider.of(context), + listener: (BuildContext context, AuthenticationState state) { + if (state is AuthenticationAuthenticated) { + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: AppStyles.successColor, + content: Text(CVLocalizations.of(context).authLoginSucceed), + )); + Future.delayed(const Duration(seconds: 1)) + .then((_) => Navigator.of(context).pop()); + } + }, + ), + ], + child: SingleChildScrollView( + child: Container( + height: screenHeight, + child: Stack( + children: [ + _buildHeaderSection(context), + _buildAuthSection(context) + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeaderSection(BuildContext context) { + return Container( + height: screenHeight * 0.25, + width: screenWidth, + ); + } + + Widget _buildAuthSection(BuildContext context) { + return Stack( + children: [ + _buildAuthPageView(context), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + height: screenHeight * 0.25, + ), + Container( + width: 300.0, + height: 50.0, + decoration: BoxDecoration( + color: const Color(0x552B2B2B), + borderRadius: const BorderRadius.all(Radius.circular(25.0)), + ), + child: CustomPaint( + painter: TabIndicationPainter(pageController: _pageController), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: FlatButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: _onSignInButtonPress, + child: Text( + CVLocalizations.of(context).authBubbleLoginCTA, + style: TextStyle( + color: left, + fontSize: 16.0, + ), + ), + ), + ), + //Container(height: 33.0, width: 1.0, color: Colors.white), + Expanded( + child: FlatButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: _onSignUpButtonPress, + child: Text( + CVLocalizations.of(context).authBubbleRegisterCTA, + style: TextStyle( + color: right, + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ], + ); + } + + Widget _buildAuthPageView(BuildContext context) { + return PageView( + controller: _pageController, + onPageChanged: (i) { + if (i == 0) { + setState(() { + right = Colors.white; + left = Colors.black; + }); + } else if (i == 1) { + setState(() { + right = Colors.black; + left = Colors.white; + }); + } + }, + children: [ + Column( + children: [ + Container( + height: screenHeight * 0.25, + ), + Container( + height: screenHeight * 0.05, + ), + Container( + height: screenHeight * 0.70, + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [LoginForm()], + ), + ), + ], + ), + Column( + children: [ + Container( + height: screenHeight * 0.25, + ), + Container( + height: screenHeight * 0.05, + ), + Container( + height: screenHeight * 0.70, + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [RegisterForm()], + ), + ), + ], + ), + ], + ); + } + + void _onSignInButtonPress() { + _pageController.animateToPage(0, + duration: const Duration(milliseconds: 500), curve: Curves.decelerate); + } + + void _onSignUpButtonPress() { + _pageController?.animateToPage(1, + duration: const Duration(milliseconds: 500), curve: Curves.decelerate); + } +} diff --git a/lib/src/presentation/pages/elements/entry_profile_page.dart b/lib/src/presentation/pages/elements/entry_profile_page.dart new file mode 100644 index 0000000..523ed6f --- /dev/null +++ b/lib/src/presentation/pages/elements/entry_profile_page.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class EntryPage extends EntryWidget { + EntryPage({Key key, String entryId, EntryEntity entry, EntryBloc entryBloc}) + : super(key: key, entryId: entryId, entry: entry, entryBloc: entryBloc); + + @override + State createState() => _EntryPageState(); +} + +class _EntryPageState extends EntryWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: entryBloc, + builder: (BuildContext context, EntryState state) { + if (state is EntryLoading) { + return Scaffold( + appBar: AppBar( + title: const LoadingShadowContent( + numberOfTitleLines: 1, + numberOfContentLines: 0, + ), + ), + body: SingleChildScrollView( + child: const LoadingShadowContent( + numberOfContentLines: 2, + padding: const EdgeInsets.all(10.0), + ), + ), + ); + } else if (state is EntryLoaded) { + final EntryEntity model = state.element; + + return Scaffold( + appBar: AppBar(title: Text(model.name)), + body: ListView( + children: [ + ListTile( + title: const Text('name'), + subtitle: Text(model.name), + ), + ListTile( + title: const Text('type'), + subtitle: Text(model.type), + ), + ListTile( + title: const Text('content'), + subtitle: Text(model.content.toString()), + ), + ], + ), + ); + } + return ErrorCard(error: NotImplementedYetError()); + }, + ); + } +} diff --git a/lib/src/presentation/pages/elements/group_profile_page.dart b/lib/src/presentation/pages/elements/group_profile_page.dart new file mode 100644 index 0000000..2af41e3 --- /dev/null +++ b/lib/src/presentation/pages/elements/group_profile_page.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/entry_profile_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/group_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/error_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/loading_widget.dart'; + +class GroupPage extends GroupWidget { + GroupPage({Key key, String groupId, GroupEntity group, GroupBloc groupBloc}) + : super(key: key, groupId: groupId, group: group, groupBloc: groupBloc); + + @override + State createState() => _GroupPageState(); +} + +class _GroupPageState extends GroupWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: groupBloc, + builder: (BuildContext context, GroupState state) { + if (state is GroupLoading) { + return Scaffold( + appBar: AppBar( + title: const LoadingShadowContent( + numberOfTitleLines: 1, + numberOfContentLines: 0, + ), + ), + body: SingleChildScrollView( + child: SingleChildScrollView( + child: const LoadingShadowContent( + numberOfContentLines: 2, + padding: EdgeInsets.all(10.0), + ), + ), + ), + ); + } else if (state is GroupLoaded) { + var model = state.element; + return Scaffold( + appBar: AppBar(title: Text(model.name)), + body: ListView.builder( + itemCount: model.entryIds.length, + itemBuilder: (BuildContext context, int index) => + EntryProfileWidget(entryId: model.entryIds[index]), + ), + ); + } + return ErrorCard(error: NotImplementedYetError()); + }, + ); + } +} diff --git a/lib/src/presentation/pages/elements/part_profile_page.dart b/lib/src/presentation/pages/elements/part_profile_page.dart new file mode 100644 index 0000000..a1b1c82 --- /dev/null +++ b/lib/src/presentation/pages/elements/part_profile_page.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/loading_widget.dart'; + +class PartProfilePage extends PartWidget { + const PartProfilePage( + {Key key, String partId, PartEntity part, PartBloc partBloc}) + : super(key: key, partId: partId, part: part, partBloc: partBloc); + + @override + State createState() => _PartProfilePageState(); +} + +class _PartProfilePageState extends PartWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: partBloc, + builder: (BuildContext context, PartState state) { + if (state is PartLoading) { + return Scaffold( + appBar: AppBar( + title: const LoadingShadowContent( + numberOfTitleLines: 1, + ), + ), + body: SingleChildScrollView( + child: SingleChildScrollView( + child: const LoadingShadowContent( + numberOfContentLines: 2, + padding: EdgeInsets.all(10.0), + ), + ), + ), + ); + } else if (state is PartLoaded) { + final PartEntity model = state.element; + + return Scaffold( + appBar: AppBar(title: Text(model.name)), + body: ListView.builder( + itemBuilder: (BuildContext context, int index) { + return Container(); + }, + ), + ); + } + return Container(); + }, + ); + } +} diff --git a/lib/src/presentation/pages/elements/profile_profile_page.dart b/lib/src/presentation/pages/elements/profile_profile_page.dart new file mode 100644 index 0000000..01cc2ba --- /dev/null +++ b/lib/src/presentation/pages/elements/profile_profile_page.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// TODO : Build owner interaction with ProfileEntity.owner + +class ProfileProfilePage extends ProfileWidget { + ProfileProfilePage( + {Key key, + String profileId, + ProfileEntity profile, + ProfileBloc profileBloc}) + : super( + key: key, + profileId: profileId, + profile: profile, + profileBloc: profileBloc); + + @override + State createState() => _ProfileProfilePageState(); +} + +class _ProfileProfilePageState extends ProfileWidgetState { + @override + Widget build(BuildContext context) { + Logger.log('Building ProfilePage'); + + return BlocBuilder( + bloc: profileBloc, + builder: (BuildContext context, ProfileState state) { + List slivers = []; + + if (state is ProfileLoaded) { + slivers.add(_ProfilePageAppBar(profile: state.element)); + ProfileEntity profile = state.element; + slivers.addAll(profile.partIds + .map((partId) => + SliverToBoxAdapter(child: PartProfileWidget(partId: partId))) + .toList()); + } else if (state is ProfileFailure) { + slivers.add( + SliverToBoxAdapter( + child: ErrorCard(error: state.error), + ), + ); + } + return Scaffold( + body: CustomScrollView( + slivers: slivers, + ), + ); + }, + ); + } +} + +class _ProfilePageAppBar extends StatelessWidget { + final ProfileEntity profile; + + _ProfilePageAppBar({@required this.profile}); + + @override + Widget build(BuildContext context) { + Widget titleWidget = LoadingShadowContent( + numberOfTitleLines: 1, + numberOfContentLines: 0, + ); + + Widget subtitleWidget = LoadingShadowContent( + numberOfTitleLines: 1, + numberOfContentLines: 0, + ); + + Widget avatarWidget = InitialCircleAvatar( + elevation: AppStyles.profileAvatarElevation, + maxRadius: AppStyles.profileAvatarMax, + minRadius: AppStyles.profileAvatarMin, + backgroundImage: AssetImage('images/default-avatar.png'), + ); + + Widget bannerWidget = Image.asset( + 'images/default-banner.jpg', + fit: BoxFit.cover, + ); + + if (this.profile != null) { + titleWidget = Text( + profile.title, + ); + subtitleWidget = Text( + profile.subtitle, + ); + avatarWidget = InitialCircleAvatar( + text: profile.title, + elevation: AppStyles.profileAvatarElevation, + maxRadius: AppStyles.profileAvatarMax, + minRadius: AppStyles.profileAvatarMin, + backgroundImage: NetworkImage(profile.picture.toString()), + ); + + bannerWidget = Image.network( + profile.cover.toString(), + fit: BoxFit.cover, + ); + } + + Widget profileInfo = Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + titleWidget, + subtitleWidget, + ], + ); + + Widget backgroundWidget = Stack( + children: [ + Stack( + fit: StackFit.expand, + children: [ + bannerWidget, + const DecoratingBackground(), + ], + ), + Center(heightFactor: 2, child: avatarWidget), + ], + ); + + return SliverAppBar( + expandedHeight: 200, + pinned: true, + elevation: 2.0, + floating: false, + flexibleSpace: FlexibleSpaceBar( + background: backgroundWidget, + collapseMode: CollapseMode.parallax, + centerTitle: true, + title: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + profileInfo, + ], + ), + ), + ), + ); + } +} + +class DecoratingBackground extends StatelessWidget { + const DecoratingBackground({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + /// This gradient ensures that the toolbar icons are distinct + /// against the background image. + begin: Alignment(0.0, -1.0), + end: Alignment(0.0, 5), + colors: [Color(0x60000000), Color(0x00000000)], + ), + ), + ); + } +} diff --git a/lib/src/pages/home_page.dart b/lib/src/presentation/pages/home_page.dart similarity index 72% rename from lib/src/pages/home_page.dart rename to lib/src/presentation/pages/home_page.dart index a52cc57..6036475 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/presentation/pages/home_page.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; class HomePage extends StatelessWidget { - const HomePage({ + final String _tag = '$HomePage'; + + HomePage({ Key key, }) : super(key: key); @override Widget build(BuildContext context) { - logger.info('Building HomePage'); + Logger.log('$_tag:build'); + return SafeArea( left: false, right: false, @@ -18,8 +20,9 @@ class HomePage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Card( + elevation: AppStyles.cardDefaultElevation, child: Container( - padding: EdgeInsets.all(25.0), + padding: AppStyles.cardDefaultPadding, child: Text( CVLocalizations.of(context).homeWelcome, style: Theme.of(context).textTheme.body2, diff --git a/lib/src/presentation/pages/main_page.dart b/lib/src/presentation/pages/main_page.dart new file mode 100644 index 0000000..d428d2c --- /dev/null +++ b/lib/src/presentation/pages/main_page.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class MainPage extends StatefulWidget { + const MainPage({Key key}) : super(key: key); + + @override + State createState() => _MainPageState(); +} + +class _MainPageState extends State { + final String _tag = '$_MainPageState'; + int _currentIndex = 0; + + final List _children = [ + HomePage(), + AccountPage(), + ]; + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return Scaffold( + appBar: AppBar( + title: Text(CVLocalizations.of(context).appName), + centerTitle: true, + actions: [ + MenuButton(), + ], + ), + body: _children.elementAt(_currentIndex), + floatingActionButton: FloatingActionButton.extended( + heroTag: AppHeroes.searchFab, + icon: Icon(Icons.search), + label: Text(CVLocalizations.of(context).search), + foregroundColor: Colors.white, + onPressed: () => navigateToSearch(context), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: Theme( + data: Theme.of(context).copyWith( + ///sets the background color of the `BottomNavigationBar` + canvasColor: Theme.of(context).primaryColor, + + ///sets the active color of the `BottomNavigationBar` if `Brightness` is light + primaryColor: Theme.of(context).selectedRowColor, + textTheme: Theme.of(context).primaryTextTheme, + ), + + ///sets the inactive color of the `BottomNavigationBar` + child: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: _onTabTapped, + type: BottomNavigationBarType.fixed, + items: [ + BottomNavigationBarItem( + icon: Icon(MdiIcons.homeOutline), + activeIcon: Icon(MdiIcons.home), + title: Text(CVLocalizations.of(context).homeCTA), + ), + const BottomNavigationBarItem( + ///Fake item + icon: SizedBox(), + title: SizedBox(), + ), + BottomNavigationBarItem( + icon: Icon(MdiIcons.accountOutline), + activeIcon: Icon(MdiIcons.account), + title: Text(CVLocalizations.of(context).accountCTA), + ), + ], + ), + ), + ); + } + + void _onTabTapped(int index) { + if (index == 0) { + setState(() { + _currentIndex = 0; + }); + } else if (index == 2) { + setState(() { + _currentIndex = 1; + }); + } + } +} diff --git a/lib/src/presentation/pages/search_page.dart b/lib/src/presentation/pages/search_page.dart new file mode 100644 index 0000000..3d5a218 --- /dev/null +++ b/lib/src/presentation/pages/search_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Add controller for the input +class SearchPage extends StatelessWidget { + final String _tag = '$SearchPage'; + + SearchPage({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text(CVLocalizations.of(context).searchTitle), + ), + SliverToBoxAdapter( + child: Hero( + tag: AppHeroes.searchFab, + child: Card( + child: Container( + padding: const EdgeInsets.all(10.0), + child: TextField( + onSubmitted: null, + autofocus: true, + decoration: InputDecoration( + labelText: CVLocalizations.of(context).search, + prefixIcon: Icon(Icons.search), + hintText: CVLocalizations.of(context).searchSearchBarHint, + ), + ), + ), + ), + ), + ), + + /// TODO: Add search result +// SliverToBoxAdapter( +// child: BlocProvider>( +// bloc: ElementBloc( +// cvRepository: _repositories.cvRepository, +// ), +// child: ProfileListWidget( +// fromSearch: '', +// showOptions: true, +// shrinkWrap: true, +// physics: ClampingScrollPhysics(), +// ), +// ), +// ), + ], + ), + ); + } +} diff --git a/lib/src/pages/settings_page.dart b/lib/src/presentation/pages/settings_page.dart similarity index 63% rename from lib/src/pages/settings_page.dart rename to lib/src/presentation/pages/settings_page.dart index 22ed282..7550a15 100644 --- a/lib/src/pages/settings_page.dart +++ b/lib/src/presentation/pages/settings_page.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/widgets/theme_switch_tile_widget.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({ @@ -10,7 +8,7 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - logger.info('Building SettingsPage'); + Logger.log('Building SettingsPage'); return Scaffold( appBar: AppBar( title: Text(CVLocalizations.of(context).settingsTitle), @@ -20,7 +18,7 @@ class SettingsPage extends StatelessWidget { right: false, child: ListView( children: [ - ThemeSwitchTile(), + const ThemeSwitchTile(), AboutListTile(icon: Icon(Icons.info)), ], ), diff --git a/lib/src/presentation/router.dart b/lib/src/presentation/router.dart new file mode 100644 index 0000000..47e9d99 --- /dev/null +++ b/lib/src/presentation/router.dart @@ -0,0 +1,119 @@ +import 'package:fluro/fluro.dart'; +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class AppRouter { + final String _tag = '$AppRouter'; + final FluroRouter router = FluroRouter(); + + AppRouter() { + _defineRoutes(); + } + + void _defineRoutes() { + Logger.log('$_tag:_defineRoutes'); + + router.define( + AppPaths.kPathHome, + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + Logger.log('Navigate to ${AppPaths.kPathHome}'); + return const MainPage(); + }, + ), + ); + + router.define( + AppPaths.kPathAccount, + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + Logger.log('Navigate to ${AppPaths.kPathAccount}'); + return const MainPage(); + }, + ), + ); + + router.define( + AppPaths.kPathLogin, + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + Logger.log('Navigate to ${AppPaths.kPathLogin}'); + return AuthPage(); + }, + ), + ); + + router.define( + AppPaths.kPathSettings, + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + Logger.log('Navigate to ${AppPaths.kPathSettings}'); + return SettingsPage(); + }, + ), + ); + + router.define( + AppPaths.kPathSearch, + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + Logger.log('Navigate to ${AppPaths.kPathSearch}'); + return SearchPage(); + }, + ), + ); + + router.define( + '${AppPaths.kPathProfiles}/:${AppPaths.kParamProfileId}', + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + final String profileId = + params[AppPaths.kParamProfileId][0] as String; + + Logger.log('Navigate to ${AppPaths.kPathProfiles}/$profileId'); + + return ProfileProfilePage(profileId: profileId); + }, + ), + ); + + router.define( + '${AppPaths.kPathParts}/:${AppPaths.kParamPartId}', + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + final String partId = params[AppPaths.kParamPartId][0] as String; + + Logger.log('Navigate to ${AppPaths.kPathParts}/$partId'); + + return PartProfilePage(partId: partId); + }, + ), + ); + + router.define( + '${AppPaths.kPathGroups}/:${AppPaths.kParamGroupId}', + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + final String groupId = params[AppPaths.kParamGroupId][0] as String; + + Logger.log('Navigate to ${AppPaths.kPathGroups}/$groupId'); + + return GroupPage(groupId: groupId); + }, + ), + ); + + router.define( + '${AppPaths.kPathEntries}/:${AppPaths.kParamEntryId}', + handler: Handler( + handlerFunc: (BuildContext context, Map params) { + final String entryId = params[AppPaths.kParamEntryId][0] as String; + + Logger.log('Navigate to ${AppPaths.kPathEntries}/$entryId'); + + return EntryPage(entryId: entryId); + }, + ), + ); + } +} diff --git a/lib/src/presentation/utils/logger.dart b/lib/src/presentation/utils/logger.dart new file mode 100644 index 0000000..29a7ec7 --- /dev/null +++ b/lib/src/presentation/utils/logger.dart @@ -0,0 +1,239 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +//////////////////////////////////////////////////////////////////////////////// +// // +// Helper classes and enums // +// // +//////////////////////////////////////////////////////////////////////////////// + +enum LogType { + log, + info, + warning, + error, + fatal, +} + +class ErrorCodes { + static const int UNHANDLED_EXCEPTION = 1; + static const int LOGIN_FAILED = 20; + static const int DOWNLOAD_FAILED = 30; + static const int OPEN_DB_FAILED = 40; +} + +//////////////////////////////////////////////////////////////////////////////// +// // +// Log entry // +// // +//////////////////////////////////////////////////////////////////////////////// + +/// A class that define a log entry +class LogEntry { + final String title; + final String message; + final int errorCode; + final Duration time; + final StackTrace stackTrace; + final LogType type; + + LogEntry({ + this.title, + this.message, + @required this.type, + @required this.time, + this.errorCode, + this.stackTrace, + }); + + @override + String toString() { + final err = errorCode != null + ? (errorCode == ErrorCodes.UNHANDLED_EXCEPTION + ? "Unhandled Exception\n" + : "Error code: $errorCode. ") + : ""; + final trace = stackTrace != null ? "\n$stackTrace" : ''; + + final mess = (title != null) ? '$title:$message' : message; + + return '[${time.toString()}] $err $mess ${trace ?? ''}'; + } +} + +/// ---------------------------------------------------------------------------- +/// Logger +/// ---------------------------------------------------------------------------- + +/// A class that provide logging services +class Logger { + static const _LOG_LENGTH = 256; + static const _ACTIONS_LOG_LENGTH = 10; + static final _instance = Logger._newInstance(); + + final DateTime _startupTime = DateTime.now(); + + final List _messagesLog = List(_LOG_LENGTH); + + int nextLogPos = 0; + int nextActionPos = 0; + + static const String INFO = "info"; + static const String ERROR = "error"; + + Duration get runtime { + return DateTime.now().difference(_startupTime); + } + + /// -------------------------------------------------------------------------- + /// Constructor + /// -------------------------------------------------------------------------- + Logger._newInstance() : super(); + + factory Logger() { + return _instance; + } + + /// -------------------------------------------------------------------------- + /// Initialization + /// -------------------------------------------------------------------------- + + /// + /// Init local log file + /// + FutureOr init() async { + //_logFile = await localStorageManager.getDocumentsFile("log.txt"); + } + + /// -------------------------------------------------------------------------- + /// General log + /// -------------------------------------------------------------------------- + + /// Print the message and add a new log entry to the log list + void doLog( + String message, { + String title, + LogType type = LogType.log, + int errorCode, + StackTrace stackTrace, + }) { + if (message == null || message == "" || message == " ") { + message = " ----"; + } + final entry = LogEntry( + message: message, + title: title, + type: type, + time: runtime, + errorCode: errorCode, + stackTrace: stackTrace, + ); + + _messagesLog[nextLogPos] = entry; + nextLogPos = (nextLogPos + 1) % _messagesLog.length; + + print(entry); + } + + /// + /// Static log function + /// + static void log(String message) => _instance.doLog(message); + + /// ----------------------------------------------------------------------- + /// Warning + /// ----------------------------------------------------------------------- + + /// + /// Do warning log. Use this when something is (probably) wrong + /// but the execution will likely be continued without any serious + /// errors. + /// + void doWarning(String message, {StackTrace stackTrace}) { + doLog(message, type: LogType.warning, stackTrace: stackTrace); + } + + /// + /// Static warning function + /// + static void warning(String message, {StackTrace stackTrace}) => + _instance.doWarning(message, stackTrace: stackTrace); + + /// ----------------------------------------------------------------------- + /// Info + /// ----------------------------------------------------------------------- + + /// Do info log. Use this for general information like startup information. + void doInfo(String message, + {String title, int autoCloseSeconds = -1, StackTrace stackTrace}) { + doLog(message, + title: title ?? INFO, type: LogType.info, stackTrace: stackTrace); + } + + /// Static info function + static void info(String message, {String title, int autoCloseSeconds = -1}) { + _instance.doInfo( + message, + title: title, + autoCloseSeconds: autoCloseSeconds, + ); + } + + // ----------------------------------------------------------------------- + // Error + // ----------------------------------------------------------------------- + + /// Do error log. Something went wrong, the app will not continue the + /// way it is meant to be. + void doError( + String message, + errorCode, { + String title, + autoCloseSeconds = -1, + StackTrace stackTrace, + }) { + final title = "$ERROR ${errorCode != 0 ? errorCode : ''}"; + + doLog(message, title: title, type: LogType.error, stackTrace: stackTrace); + } + + /// Static error function + static void error(String message, {int errorCode, StackTrace stackTrace}) => + _instance.doError(message, errorCode, stackTrace: stackTrace); + + // ----------------------------------------------------------------------- + // Fatal + // ----------------------------------------------------------------------- + + /// Fatal error. Something went wrong, the app will not continue the + /// way it is meant to be and a mail with error details must be + /// send to us. + void doFatal(String message, int errorCode, {StackTrace stackTrace}) { + doLog(message, + type: LogType.fatal, errorCode: errorCode, stackTrace: stackTrace); + } + + /// Static fatal function + static void fatal(String message, {int errorCode, StackTrace stackTrace}) => + _instance.doFatal(message, errorCode, stackTrace: stackTrace); + + /// ----------------------------------------------------------------------- + /// Log list + /// ----------------------------------------------------------------------- + + static get logList => _instance._logList; + + List get _logList { + final List logs = _messagesLog; + logs.removeWhere((e) => e == null); + logs.sort((a, b) => a.time > b.time ? 1 : -1); + return logs; + } + + static get logString => _instance._logString; + + get _logString { + return _logList.join("\n"); + } +} diff --git a/lib/src/presentation/utils/navigation.dart b/lib/src/presentation/utils/navigation.dart new file mode 100644 index 0000000..baf326e --- /dev/null +++ b/lib/src/presentation/utils/navigation.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +void navigateToLogin(BuildContext context) { + Navigator.of(context).pushNamed(AppPaths.kPathLogin); +} + +void navigateToSettings(BuildContext context) { + Navigator.of(context).pushNamed(AppPaths.kPathSettings); +} + +void navigateToSearch(BuildContext context) { + Navigator.of(context).pushNamed(AppPaths.kPathSearch); +} + +void navigateToProfile(BuildContext context, String profileId) { + Navigator.of(context) + .pushNamed('${AppPaths.kPathProfiles}/${profileId ?? ''}'); +} + +void navigateToPart(BuildContext context, String partId) { + Navigator.of(context).pushNamed('${AppPaths.kPathParts}/${partId ?? ''}'); +} + +void navigateToGroup(BuildContext context, + {String groupId, GroupEntity group}) { + assert(groupId != null || group != null); + if (group != null) { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => GroupPage(group: group))); +// Navigator.of(context).pushNamed( +// AppPaths.kPathGroups + '/${group.id ?? ''}', +// arguments: group, +// ); + } else if (groupId != null) { + Navigator.of(context).pushNamed( + '${AppPaths.kPathGroups}/${groupId ?? ''}', + ); + } +} + +void navigateToEntry(BuildContext context, + {String entryId, EntryEntity entry}) { + assert(entryId != null || entry != null); + if (entry != null) { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => EntryPage(entry: entry))); +// Navigator.of(context).pushNamed( +// AppPaths.kPathEntries + '/${entry.id ?? ''}', +// arguments: entry, +// ); + } else if (entryId != null) { + Navigator.of(context) + .pushNamed('${AppPaths.kPathEntries}/${entryId ?? ''}'); + } +} + +void openMenuBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (context) => MenuBottomSheet(), + ); +} diff --git a/lib/src/presentation/utils/translate_error.dart b/lib/src/presentation/utils/translate_error.dart new file mode 100644 index 0000000..fae7b1e --- /dev/null +++ b/lib/src/presentation/utils/translate_error.dart @@ -0,0 +1,185 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// Translate [Error] and [Exception] +String translateError(BuildContext context, dynamic err) { + Logger.log('Translating error'); + + final CVLocalizations loc = CVLocalizations.of(context); + + if (loc == null) return err.toString(); + + String txt; + + if (err is Exception) { + // Exceptions + if (err is AppException) { + txt = _translateAppException(context, err); + } else if (err is HttpException) { + txt = _translateHttpException(context, err); + } else if (err is FormatException) { + txt = loc.exceptionFormatException; + } else if (err is TimeoutException) { + txt = loc.exceptionTimeoutException; + } + } else if (err is Error) { + // Errors + if (err is NotImplementedYetError) { + txt = loc.errorNotYetImplemented; + } else if (err is NotSupportedError) { + txt = loc.errorNotSupported; + } + } else if (err is String) { + txt = err; + } else { + txt = '${err.runtimeType}'; + } + + return txt ??= + '${loc.errorOccurred}: ${err.runtimeType}${err?.message != null ? ' "${err?.message}"' : ''}'; +} + +/// Translate [HttpException] +String _translateHttpException(BuildContext context, HttpException err) { + final CVLocalizations loc = CVLocalizations.of(context); + String txt; + switch (err.statusCode) { + case HttpStatus.badRequest: + // HTTP 400 + txt = loc.http400ClientErrorBadRequest; + break; + case HttpStatus.unauthorized: + // HTTP 401 + txt = loc.http401ClientErrorUnauthorized; + break; + case HttpStatus.paymentRequired: + // HTTP 402 + txt = loc.http402ClientErrorPaymentRequired; + break; + case HttpStatus.forbidden: + // HTTP 403 + txt = loc.http403ClientErrorForbidden; + break; + case HttpStatus.notFound: + // HTTP 404 + txt = loc.http404ClientErrorNotFound; + break; + case HttpStatus.methodNotAllowed: + // HTTP 405 + txt = loc.http405ClientErrorMethodNotAllowed; + break; + case HttpStatus.notAcceptable: + // HTTP 406 + txt = loc.http406ClientErrorNotAcceptable; + break; + case HttpStatus.requestTimeout: + // HTTP 408 + txt = loc.http408ClientErrorRequestTimeout; + break; + case HttpStatus.conflict: + // HTTP 409 + txt = loc.http409ClientErrorConflict; + break; + case HttpStatus.gone: + // HTTP 410 + txt = loc.http410ClientErrorGone; + break; + case HttpStatus.lengthRequired: + // HTTP 411 + txt = loc.http411ClientErrorLengthRequired; + break; + case HttpStatus.requestEntityTooLarge: + // HTTP 413 + txt = loc.http413ClientErrorPayloadTooLarge; + break; + case HttpStatus.requestUriTooLong: + // HTTP 414 + txt = loc.http414ClientErrorURITooLong; + break; + case HttpStatus.unsupportedMediaType: + // HTTP 415 + txt = loc.http415ClientErrorUnsupportedMediaType; + break; + case HttpStatus.expectationFailed: + // HTTP 417 + txt = loc.http417ClientErrorExpectationFailed; + break; + case HttpStatus.upgradeRequired: + // HTTP 426 + txt = loc.http426ClientErrorUpgradeRequired; + break; + case HttpStatus.internalServerError: + // HTTP 500 + txt = loc.http500ServerErrorInternalServerError; + break; + case HttpStatus.notImplemented: + // HTTP 501 + txt = loc.http501ServerErrorNotImplemented; + break; + case HttpStatus.badGateway: + // HTTP 502 + txt = loc.http502ServerErrorBadGateway; + break; + case HttpStatus.serviceUnavailable: + // HTTP 503 + txt = loc.http503ServerErrorServiceUnavailable; + break; + case HttpStatus.gatewayTimeout: + // HTTP 504 + txt = loc.http504ServerErrorGatewayTimeout; + break; + case HttpStatus.httpVersionNotSupported: + // HTTP 505 + txt = loc.http505ServerErrorHttpVersionNotSupported; + break; + } + return txt; +} + +/// Translate [AppException] +String _translateAppException(BuildContext context, AppException err) { + final CVLocalizations loc = CVLocalizations.of(context); + String txt; + + switch (err.type) { + case AppExceptionType.somethingWentWrong: + txt = loc.errorOccurred; + break; + case AppExceptionType.authNoToken: + txt = loc.appErrorAuthNoToken; + break; + case AppExceptionType.authLoginFailed: + txt = loc.authLoginFailed; + break; + case AppExceptionType.authRegistrationFailed: + txt = loc.authRegisterFailed; + break; + case AppExceptionType.authAccountAlreadyExists: + txt = loc.authAccountAlreadyExistsFailure; + break; + case AppExceptionType.authAccountDisabled: + txt = loc.appErrorAuthAccountDisabled; + break; + case AppExceptionType.authUnauthorized: + txt = loc.appErrorAuthUnauthorized; + break; + case AppExceptionType.authForbidden: + txt = loc.appErrorAuthForbidden; + break; + case AppExceptionType.userNotFound: + txt = loc.appErrorUserNotFound; + break; + case AppExceptionType.formPasswordWrongPolicy: + txt = loc.formPasswordWrongPolicy; + break; + case AppExceptionType.serverSideProblem: + txt = loc.appErrorServerSideProblem; + break; + } + + return txt; +} diff --git a/lib/src/presentation/utils/utils.dart b/lib/src/presentation/utils/utils.dart new file mode 100644 index 0000000..fc5ea6e --- /dev/null +++ b/lib/src/presentation/utils/utils.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/src/presentation/commons/defaults.dart'; + +String getInitials(String nameString) { + if (nameString.isEmpty) return ' '; + + final List nameArray = + nameString.replaceAll(RegExp(r'\s+\b|\b\s'), ' ').split(' '); + final String initials = + ((nameArray[0])[0] != null ? (nameArray[0])[0] : ' ') + + (nameArray.length == 1 ? ' ' : (nameArray[nameArray.length - 1])[0]); + + return initials; +} + +List> getDropDownMenuElementPerPage() { + final List _values = [ + kCVItemsPerPage1, + kCVItemsPerPage2, + kCVItemsPerPage3, + kCVItemsPerPage4 + ]; + final List> items = List(); + for (String value in _values) { + items.add(DropdownMenuItem(value: value, child: Text(value))); + } + return items; +} diff --git a/lib/src/utils/validators.dart b/lib/src/presentation/utils/validators.dart similarity index 89% rename from lib/src/utils/validators.dart rename to lib/src/presentation/utils/validators.dart index fe2c7fa..bd16dac 100644 --- a/lib/src/utils/validators.dart +++ b/lib/src/presentation/utils/validators.dart @@ -24,11 +24,11 @@ class Validators { final validatePassword = StreamTransformer.fromHandlers( handleData: (password, sink) { - print('PerformPasswordValidation : $password'); + print('PerformPasswordValidation : HIDDEN'); if (password.isEmpty) { sink.addError(ValidationErrors.ERROR_LOGIN_NO_PASSWORD); } else { - print('PerformPasswordValidation : $password is correct'); + print('PerformPasswordValidation : HIDDEN is correct'); sink.add(password); } }); diff --git a/lib/src/presentation/widgets/account_tile_widget.dart b/lib/src/presentation/widgets/account_tile_widget.dart new file mode 100644 index 0000000..30b76b1 --- /dev/null +++ b/lib/src/presentation/widgets/account_tile_widget.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class AccountTile extends StatefulWidget { + const AccountTile({Key key}) : super(key: key); + + @override + State createState() => _AccountTitleState(); +} + +class _AccountTitleState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, AuthenticationState state) { + if (state is AuthenticationAuthenticated) { + return _AccountTileConnected(); + } else if (state is AuthenticationUnauthenticated) { + return _AccountTileNotConnected(); + } + return ErrorTile(error: NotImplementedYetError()); + }, + ); + } +} + +/// ---------------------------------------------------------------------------- +/// _AccountTileConnected +/// ---------------------------------------------------------------------------- + +class _AccountTileConnected extends StatelessWidget { + final String _tag = '$_AccountTileConnected'; + + _AccountTileConnected({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, IdentityState state) { + if (state is IdentityUninitialized) { + } else if (state is IdentityLoaded) { + var userModel = state.user; + return ListTile( + leading: InitialCircleAvatar( + text: userModel.username, + backgroundImage: NetworkImage(userModel.picture), + ), + title: Text(userModel.username), + subtitle: Text(userModel.email), + trailing: IconButton( + icon: Icon(MdiIcons.logout), + onPressed: () => null, // TODO: Add logout + ), + ); + } else if (state is IdentityFailed) { + return ErrorTile(error: state.error); + } + return ErrorTile(error: NotImplementedYetError()); + }, + ); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// // +// _AccountTileNotConnected // +// // +//////////////////////////////////////////////////////////////////////////////// +class _AccountTileNotConnected extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GestureDetector( + child: ListTile( + title: Center(child: Text(CVLocalizations.of(context).authLoginCTA)), + trailing: Icon(MdiIcons.login), + ), + onTap: () => navigateToLogin(context), + ); + } +} diff --git a/lib/src/widgets/arc_banner_image_widget.dart b/lib/src/presentation/widgets/arc_banner_image_widget.dart similarity index 53% rename from lib/src/widgets/arc_banner_image_widget.dart rename to lib/src/presentation/widgets/arc_banner_image_widget.dart index c1b488c..bdd8a44 100644 --- a/lib/src/widgets/arc_banner_image_widget.dart +++ b/lib/src/presentation/widgets/arc_banner_image_widget.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; class ArcBannerImage extends StatelessWidget { - const ArcBannerImage({ - Key key, - @required this.image, - }) : super(key: key); + final String _tag = '$ArcBannerImage'; - final ImageProvider image; + final ImageProvider imageProvider; + + ArcBannerImage({Key key, @required this.imageProvider}) + : assert(imageProvider != null, 'No $ImageProvider given'), + super(key: key); @override Widget build(BuildContext context) { - var screenWidth = MediaQuery.of(context).size.width; + Logger.log('$_tag:build'); + + final screenWidth = MediaQuery.of(context).size.width; return ClipPath( clipper: ArcClipper(), child: Image( - image: image, + image: imageProvider, width: screenWidth, height: 230.0, fit: BoxFit.cover, @@ -27,17 +31,17 @@ class ArcBannerImage extends StatelessWidget { class ArcClipper extends CustomClipper { @override Path getClip(Size size) { - var path = new Path(); + final path = Path(); path.lineTo(0.0, size.height - 30); - var firstControlPoint = new Offset(size.width / 4, size.height); - var firstPoint = new Offset(size.width / 2, size.height); + final firstControlPoint = Offset(size.width / 4, size.height); + final firstPoint = Offset(size.width / 2, size.height); path.quadraticBezierTo(firstControlPoint.dx, firstControlPoint.dy, firstPoint.dx, firstPoint.dy); - var secondControlPoint = - new Offset(size.width - (size.width / 4), size.height); - var secondPoint = new Offset(size.width, size.height - 30); + final secondControlPoint = + Offset(size.width - (size.width / 4), size.height); + final secondPoint = Offset(size.width, size.height - 30); path.quadraticBezierTo(secondControlPoint.dx, secondControlPoint.dy, secondPoint.dx, secondPoint.dy); diff --git a/lib/src/presentation/widgets/bubble_indication_painter.dart b/lib/src/presentation/widgets/bubble_indication_painter.dart new file mode 100644 index 0000000..85af493 --- /dev/null +++ b/lib/src/presentation/widgets/bubble_indication_painter.dart @@ -0,0 +1,52 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class TabIndicationPainter extends CustomPainter { + Paint painter; + final double dxTarget; + final double dxEntry; + final double radius; + final double dy; + + final PageController pageController; + + TabIndicationPainter({ + this.dxTarget = 125.0, + this.dxEntry = 25.0, + this.radius = 21.0, + this.dy = 25.0, + this.pageController, + }) : super(repaint: pageController) { + painter = Paint() + ..color = const Color(0xFFFFFFFF) + ..style = PaintingStyle.fill; + } + + @override + void paint(Canvas canvas, Size size) { + final pos = pageController.position; + final double fullExtent = + pos.maxScrollExtent - pos.minScrollExtent + pos.viewportDimension; + + final double pageOffset = pos.extentBefore / fullExtent; + + final bool left2right = dxEntry < dxTarget; + final Offset entry = Offset(left2right ? dxEntry : dxTarget, dy); + final Offset target = Offset(left2right ? dxTarget : dxEntry, dy); + + final Path path = Path(); + path.addArc( + Rect.fromCircle(center: entry, radius: radius), 0.5 * pi, 1 * pi); + path.addRect(Rect.fromLTRB(entry.dx, dy - radius, target.dx, dy + radius)); + path.addArc( + Rect.fromCircle(center: target, radius: radius), 1.5 * pi, 1 * pi); + + canvas.translate(size.width * pageOffset, 0.0); + canvas.drawShadow(path, const Color(0xFFfbab66), 3.0, true); + canvas.drawPath(path, painter); + } + + @override + bool shouldRepaint(TabIndicationPainter oldDelegate) => true; +} diff --git a/lib/src/presentation/widgets/elements/entry_list_profile_widget.dart b/lib/src/presentation/widgets/elements/entry_list_profile_widget.dart new file mode 100644 index 0000000..7b5b417 --- /dev/null +++ b/lib/src/presentation/widgets/elements/entry_list_profile_widget.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [SimpleEntryListProfile] is a dummy widget that use [entryIds] or [entries] +/// or [entryBlocs] to create a list of [EntryProfileWidget] +class SimpleEntryListProfile extends StatelessWidget { + final List entryIds; + final List entries; + final List entryBlocs; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + const SimpleEntryListProfile({ + Key key, + this.entryIds, + this.entries, + this.entryBlocs, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : assert(entryIds != null && entries == null && entryBlocs == null), + assert(entryIds == null && entries != null && entryBlocs == null), + assert(entryIds == null && entries == null && entryBlocs != null), + super(key: key); + + @override + Widget build(BuildContext context) { + int itemCount; + IndexedWidgetBuilder itemBuilder; + + if (entryIds != null && entryIds.isNotEmpty) { + itemCount = entryIds.length; + itemBuilder = (BuildContext context, int index) => + EntryProfileWidget(entryId: entryIds[index]); + } else if (entries != null && entries.isNotEmpty) { + itemCount = entries.length; + itemBuilder = (BuildContext context, int index) => + EntryProfileWidget(entry: entries[index]); + } else if (entryBlocs != null && entryBlocs.isNotEmpty) { + itemCount = entryBlocs.length; + itemBuilder = (BuildContext context, int index) => + EntryProfileWidget(entryBloc: entryBlocs[index]); + } + + return ListView.builder( + scrollDirection: scrollDirection, + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + } +} + +/// [ComplexEntryListProfile] is a clever widget that use [parentGroupId] or [ownerId] or [entryListBloc] to display a list of [EntryProfileWidget] +class ComplexEntryListProfile extends EntryListWidget { + /// Search, filter and sort options + final bool showOptions; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + ComplexEntryListProfile({ + Key key, + String parentGroupId, + String ownerId, + EntryListBloc entryListBloc, + + /// Options + this.showOptions = false, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : super( + key: key, + parentGroupId: parentGroupId, + ownerId: ownerId, + entryListBloc: entryListBloc, + ); + + @override + State createState() => _EntryListProfileState(); +} + +class _EntryListProfileState + extends ComplexEntryListState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: widget.entryListBloc, + builder: (BuildContext context, EntryListState state) { + if (state is EntryListLoading) { + return LoadingList( + count: state.count, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } else if (state is EntryListLoaded) { + final List sortItems = [ + SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) + ]; + + final sortRow = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon(Icons.sort_by_alpha), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SortDialog( + title: + Text(CVLocalizations.of(context).entryListSorting), + sortItems: sortItems, + ); + }, + ); + }, + ), + DropdownButton( + value: null, + hint: Text(CVLocalizations.of(context).partListItemPerPage), + items: getDropDownMenuElementPerPage(), + onChanged: (value) {}, + ) + ], + ); + + return ListView( + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + children: [ + if (widget.showOptions) sortRow, + for (var entry in state.elements) + EntryProfileWidget(entry: entry), + if (widget.showOptions) + Center( + child: FlatButton( + onPressed: null, + child: Text(CVLocalizations.of(context).entryListLoadMore), + ), + ), + ], + ); + } else if (state is EntryListFailure) { + return ErrorList( + error: state.error, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } + + return ErrorList( + error: NotImplementedYetError(), + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/elements/entry_list_widget.dart b/lib/src/presentation/widgets/elements/entry_list_widget.dart new file mode 100644 index 0000000..b5af7af --- /dev/null +++ b/lib/src/presentation/widgets/elements/entry_list_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// [EntryListWidget] is a clever widget that use an [EntryListBloc] +/// based on [parentGroupId] or [ownerId] or [entryListBloc] +abstract class EntryListWidget extends StatefulWidget { + final String parentGroupId; + final String ownerId; + final EntryListBloc entryListBloc; + + const EntryListWidget({ + Key key, + this.parentGroupId, + this.ownerId, + this.entryListBloc, + }) : assert( + parentGroupId != null && ownerId == null && entryListBloc == null), + assert( + parentGroupId == null && ownerId != null && entryListBloc == null), + assert( + parentGroupId == null && ownerId == null && entryListBloc != null), + super(key: key); +} + +/// If [widget.entryListBloc] exists the lifecycle of it will be managed by its creator +abstract class ComplexEntryListState + extends State { + EntryListBloc entryListBloc; + + @override + void initState() { + super.initState(); + + entryListBloc = widget.entryListBloc; + + if (entryListBloc == null) { + final repo = Provider.of(context, listen: false); + entryListBloc = EntryListBloc(repository: repo); + entryListBloc.dispatch(EntryListInitialized( + parentGroupId: widget.parentGroupId, + ownerId: widget.ownerId, + )); + } + } + + @override + void dispose() { + if (widget.entryListBloc == null) entryListBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/widgets/entry_widget.dart b/lib/src/presentation/widgets/elements/entry_profile_widget.dart similarity index 56% rename from lib/src/widgets/entry_widget.dart rename to lib/src/presentation/widgets/elements/entry_profile_widget.dart index 8e50047..c996221 100644 --- a/lib/src/widgets/entry_widget.dart +++ b/lib/src/presentation/widgets/elements/entry_profile_widget.dart @@ -1,57 +1,64 @@ import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/commons/api_values.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; -class EntryWidget extends StatelessWidget { - const EntryWidget({ - Key key, - @required this.entryModel, - }) : assert(entryModel != null), - super(key: key); +/// [EntryProfileWidget] is an [EntryWidget] for profile display purpose +class EntryProfileWidget extends EntryWidget { + EntryProfileWidget( + {Key key, String entryId, EntryEntity entry, EntryBloc entryBloc}) + : super(key: key, entryId: entryId, entry: entry, entryBloc: entryBloc); - final EntryModel entryModel; + @override + State createState() => _EntryProfileWidgetState(); +} +class _EntryProfileWidgetState extends EntryWidgetState { @override Widget build(BuildContext context) { - if (entryModel.type == kCVEntryTypeMap) { - return _EntryWidgetMap(entryModel: entryModel); - } else if (entryModel.type == kCVEntryTypeEvent) { - return _EntryWidgetEvent(entryModel: entryModel); - } else if (entryModel.type == kCVEntryTypeTag) { - return _EntryWidgetTag(entryModel); - } else { - return ErrorContent(message: CVLocalizations.of(context).notSupported); - } + return BlocBuilder( + bloc: entryBloc, + builder: (BuildContext context, EntryState state) { + if (state is EntryLoaded) { + final entry = state.element; + if (entry.type == kCVEntryTypeMap) { + return _EntryWidgetMap(entry: entry); + } else if (entry.type == kCVEntryTypeEvent) { + return _EntryWidgetEvent(entry: entry); + } else if (entry.type == kCVEntryTypeTag) { + return _EntryWidgetTag(entry); + } + } + return ErrorRow(error: NotImplementedYetError()); + }, + ); } } class _EntryWidgetMap extends StatelessWidget { - _EntryWidgetMap({ - @required this.entryModel, - }) : assert(entryModel != null); + final EntryEntity entry; - final EntryModel entryModel; + _EntryWidgetMap({ + @required this.entry, + }) : assert(EntryEntity != null); @override Widget build(BuildContext context) { return InkWell( - onTap: () => navigateToEntry(context, entryModel.id), + onTap: () => navigateToEntry(context, entry: entry), child: Container( - padding: EdgeInsets.all(AppDimensions.kCVEntryPadding), + padding: AppStyles.entryPadding, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${entryModel.name ?? ''}', + '${entry.name ?? ''}', style: TextStyle(fontWeight: FontWeight.bold), ), Expanded( child: Text( - '${entryModel.content ?? ''}', + '${entry.content ?? ''}', textAlign: TextAlign.end, ), ) @@ -63,20 +70,20 @@ class _EntryWidgetMap extends StatelessWidget { } class _EntryWidgetEvent extends StatelessWidget { - _EntryWidgetEvent({ - @required this.entryModel, - }) : assert(entryModel != null); + final EntryEntity entry; - final EntryModel entryModel; + _EntryWidgetEvent({ + @required this.entry, + }) : assert(EntryEntity != null); @override Widget build(BuildContext context) { return Card( - elevation: AppDimensions.kCVEntryCardElevation, + elevation: AppStyles.entryCardElevation, child: Container( - height: AppDimensions.kCVEntryEventHeight, - width: AppDimensions.kCVEntryEventHWidth, - padding: const EdgeInsets.all(AppDimensions.kCVEntryPadding), + height: AppStyles.entryEventHeight, + width: AppStyles.entryEventHWidth, + padding: AppStyles.entryPadding, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -86,7 +93,7 @@ class _EntryWidgetEvent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${entryModel.startDate} ${entryModel.endDate}', + '${entry.startDate} ${entry.endDate}', textAlign: TextAlign.start, style: TextStyle( color: Theme.of(context).accentColor, @@ -95,7 +102,7 @@ class _EntryWidgetEvent extends StatelessWidget { ), Expanded( child: Text( - entryModel.location ?? '', + entry.location ?? '', textAlign: TextAlign.end, style: TextStyle(fontStyle: FontStyle.italic), ), @@ -103,13 +110,13 @@ class _EntryWidgetEvent extends StatelessWidget { ], ), Text( - entryModel.name ?? '', + entry.name ?? '', textAlign: TextAlign.start, style: TextStyle(fontWeight: FontWeight.bold), ), Expanded( child: Text( - entryModel.content, + entry.content as String, textAlign: TextAlign.justify, ), ), @@ -118,7 +125,7 @@ class _EntryWidgetEvent extends StatelessWidget { children: [ FlatButton( child: Text(CVLocalizations.of(context).entryWidgetDetails), - onPressed: () => navigateToEntry(context, entryModel.id), + onPressed: () => navigateToEntry(context, entry: entry), ) ], ) @@ -130,20 +137,18 @@ class _EntryWidgetEvent extends StatelessWidget { } class _EntryWidgetTag extends StatelessWidget { - _EntryWidgetTag(this.entryModel); + final EntryEntity entry; - final EntryModel entryModel; + _EntryWidgetTag(this.entry); @override Widget build(BuildContext context) { - List tags = entryModel.content; - List _tagWidgets = []; - tags.forEach((dynamic tag) { - _tagWidgets.add(Chip(label: Text(tag as String))); - }); + final List tags = entry.content as List; + final List _tagWidgets = + tags.map((tag) => Chip(label: Text(tag))).toList(); return Container( - padding: EdgeInsets.all(AppDimensions.kCVEntryPadding), + padding: AppStyles.entryPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -151,7 +156,7 @@ class _EntryWidgetTag extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - entryModel.name.toUpperCase() ?? '', + entry.name.toUpperCase() ?? '', style: TextStyle( color: Theme.of(context).accentColor, fontWeight: FontWeight.bold, @@ -159,13 +164,13 @@ class _EntryWidgetTag extends StatelessWidget { ), FlatButton( child: Text(CVLocalizations.of(context).entryWidgetDetails), - onPressed: () => navigateToEntry(context, entryModel.id), + onPressed: () => navigateToEntry(context, entry: entry), ) ], ), Wrap( alignment: WrapAlignment.start, - spacing: AppDimensions.kCVEntryTagSpacing, + spacing: AppStyles.entryTagSpacing, runSpacing: 0.0, children: _tagWidgets, ) diff --git a/lib/src/presentation/widgets/elements/entry_widget.dart b/lib/src/presentation/widgets/elements/entry_widget.dart new file mode 100644 index 0000000..13d2011 --- /dev/null +++ b/lib/src/presentation/widgets/elements/entry_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// If [entryBloc] given we assume that it have been already initialized +abstract class EntryWidget extends StatefulWidget { + final String entryId; + final EntryEntity entry; + final EntryBloc entryBloc; + + EntryWidget({Key key, this.entryId, this.entry, this.entryBloc}) + : assert(entryId != null && entry == null && entryBloc == null), + assert(entryId == null && entry != null && entryBloc == null), + assert(entryId == null && entry == null && entryBloc != null), + super(key: key); +} + +/// If [widget.entryBloc] exists the lifecycle of it will be managed by its creator +abstract class EntryWidgetState extends State { + EntryBloc entryBloc; + + @override + void initState() { + super.initState(); + + entryBloc = widget.entryBloc; + + if (entryBloc == null) { + final repo = Provider.of(context, listen: false); + + entryBloc = EntryBloc(repository: repo); + entryBloc.dispatch(EntryInitialized( + entryId: widget.entryId, + entry: widget.entry, + )); + } + } + + @override + void dispose() { + if (widget.entryBloc == null) entryBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/group_list_profile_widget.dart b/lib/src/presentation/widgets/elements/group_list_profile_widget.dart new file mode 100644 index 0000000..c2c6c34 --- /dev/null +++ b/lib/src/presentation/widgets/elements/group_list_profile_widget.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [SimpleGroupListProfile] is a dummy widget that use [groupIds] or [groups] +/// or [groupBlocs] to create a list of [GroupProfileWidget] +class SimpleGroupListProfile extends StatelessWidget { + final List groupIds; + final List groups; + final List groupBlocs; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + const SimpleGroupListProfile({ + Key key, + this.groupIds, + this.groups, + this.groupBlocs, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : assert(groupIds != null && groups == null && groupBlocs == null), + assert(groupIds == null && groups != null && groupBlocs == null), + assert(groupIds == null && groups == null && groupBlocs != null), + super(key: key); + + @override + Widget build(BuildContext context) { + int itemCount; + IndexedWidgetBuilder itemBuilder; + + if (groupIds != null && groupIds.isNotEmpty) { + itemCount = groupIds.length; + itemBuilder = (BuildContext context, int index) => + GroupProfileWidget(groupId: groupIds[index]); + } else if (groups != null && groups.isNotEmpty) { + itemCount = groups.length; + itemBuilder = (BuildContext context, int index) => + GroupProfileWidget(group: groups[index]); + } else if (groupBlocs != null && groupBlocs.isNotEmpty) { + itemCount = groupBlocs.length; + itemBuilder = (BuildContext context, int index) => + GroupProfileWidget(groupBloc: groupBlocs[index]); + } + + return ListView.builder( + scrollDirection: scrollDirection, + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + } +} + +/// [ComplexGroupListProfile] is a clever widget that use [parentPartId] or [ownerId] or [groupListBloc] to display a list of [GroupProfileWidget] +class ComplexGroupListProfile extends GroupListWidget { + /// Search, filter and sort options + final bool showOptions; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + ComplexGroupListProfile({ + Key key, + String parentPartId, + String ownerId, + GroupListBloc groupListBloc, + + /// Options + this.showOptions = false, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : super( + key: key, + parentPartId: parentPartId, + ownerId: ownerId, + groupListBloc: groupListBloc, + ); + + @override + State createState() => _GroupListProfileState(); +} + +class _GroupListProfileState + extends GroupListWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: widget.groupListBloc, + builder: (BuildContext context, GroupListState state) { + if (state is GroupListLoading) { + return LoadingList( + count: state.count, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } else if (state is GroupListLoaded) { + final List sortItems = [ + SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) + ]; + + final sortRow = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon(Icons.sort_by_alpha), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SortDialog( + title: + Text(CVLocalizations.of(context).groupListSorting), + sortItems: sortItems, + ); + }, + ); + }, + ), + DropdownButton( + value: null, + hint: Text(CVLocalizations.of(context).partListItemPerPage), + items: getDropDownMenuElementPerPage(), + onChanged: (value) {}, + ) + ], + ); + + return ListView( + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + children: [ + if (widget.showOptions) sortRow, + for (var group in state.elements) + GroupProfileWidget(group: group), + if (widget.showOptions) + Center( + child: FlatButton( + onPressed: null, + child: Text(CVLocalizations.of(context).groupListLoadMore), + ), + ), + ], + ); + } else if (state is GroupListFailure) { + return ErrorList( + error: state.error, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } + return ErrorList( + error: NotImplementedYetError(), + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/elements/group_list_widget.dart b/lib/src/presentation/widgets/elements/group_list_widget.dart new file mode 100644 index 0000000..d66621f --- /dev/null +++ b/lib/src/presentation/widgets/elements/group_list_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class GroupListWidget extends StatefulWidget { + final String parentPartId; + final String ownerId; + final GroupListBloc groupListBloc; + + GroupListWidget({ + Key key, + this.parentPartId, + this.ownerId, + this.groupListBloc, + }) : assert( + parentPartId != null && ownerId == null && groupListBloc == null), + assert( + parentPartId == null && ownerId != null && groupListBloc == null), + assert( + parentPartId == null && ownerId == null && groupListBloc != null), + super(key: key); +} + +/// If [widget.groupListBloc] exists the lifecycle of it will be managed by its creator +abstract class GroupListWidgetState + extends State { + GroupListBloc groupListBloc; + + @override + void initState() { + super.initState(); + + groupListBloc = widget.groupListBloc; + + if (widget.groupListBloc == null) { + final groupRepo = Provider.of(context, listen: false); + + groupListBloc = GroupListBloc(repository: groupRepo); + groupListBloc.dispatch(GroupListInitialized( + parentPartId: widget.parentPartId, + ownerId: widget.ownerId, + )); + } + } + + @override + void dispose() { + if (widget.groupListBloc == null) groupListBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/group_profile_widget.dart b/lib/src/presentation/widgets/elements/group_profile_widget.dart new file mode 100644 index 0000000..314b39b --- /dev/null +++ b/lib/src/presentation/widgets/elements/group_profile_widget.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [GroupProfileWidget] is an [GroupWidget] for profile display purpose +class GroupProfileWidget extends GroupWidget { + GroupProfileWidget( + {Key key, String groupId, GroupEntity group, GroupBloc groupBloc}) + : super(key: key, groupId: groupId, group: group, groupBloc: groupBloc); + + @override + State createState() => _GroupProfileWidgetState(); +} + +class _GroupProfileWidgetState extends GroupWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: groupBloc, + builder: (BuildContext context, GroupState state) { + if (state is GroupLoaded) { + GroupEntity group; + if (group.type == kCVGroupTypeListHorizontal) { + return _GroupHorizontal(group: group); + } else if (group.type == kCVGroupTypeListVertical) { + return _GroupVertical(group: group); + } else { + return ErrorRow(error: NotImplementedYetError()); + } + } + return ErrorRow(error: NotImplementedYetError()); + }, + ); + } +} + +class _GroupHorizontal extends StatelessWidget { + const _GroupHorizontal({ + @required this.group, + }) : assert(group != null); + + final GroupEntity group; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppStyles.groupHorizontalPadding, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + group.name.toUpperCase(), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + FlatButton( + child: Text(CVLocalizations.of(context).groupWidgetDetails), + onPressed: () => navigateToGroup(context, group: group), + ), + ], + ), + ), + Container( + height: AppStyles.entryHorizontalListHeight, + child: SimpleEntryListProfile( + entryIds: group.entryIds, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + ), + ), + ], + ); + } +} + +class _GroupVertical extends StatelessWidget { + const _GroupVertical({ + @required this.group, + }) : assert(group != null); + + final GroupEntity group; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: AppStyles.groupHorizontalPadding), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + group.name.toUpperCase(), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold), + ), + FlatButton( + child: Text(CVLocalizations.of(context).groupWidgetDetails), + onPressed: () => navigateToGroup(context, group: group), + ), + ], + ), + ), + Card( + elevation: 2.0, + child: SimpleEntryListProfile( + entryIds: group.entryIds, + scrollDirection: Axis.vertical, + shrinkWrap: true, + physics: ClampingScrollPhysics(), + ), + ), + ], + ); + } +} diff --git a/lib/src/presentation/widgets/elements/group_widget.dart b/lib/src/presentation/widgets/elements/group_widget.dart new file mode 100644 index 0000000..7816671 --- /dev/null +++ b/lib/src/presentation/widgets/elements/group_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// If [groupBloc] given we assume that it have been already initialized and +abstract class GroupWidget extends StatefulWidget { + final String groupId; + final GroupEntity group; + final GroupBloc groupBloc; + + GroupWidget({Key key, this.groupId, this.group, this.groupBloc}) + : assert(groupId != null && group == null && groupBloc == null), + assert(groupId == null && group != null && groupBloc == null), + assert(groupId == null && group == null && groupBloc != null), + super(key: key); +} + +/// If [widget.groupBloc] exists the lifecycle of it will be managed by its creator +abstract class GroupWidgetState extends State { + GroupBloc groupBloc; + + @override + void initState() { + super.initState(); + + groupBloc = widget.groupBloc; + + if (groupBloc == null) { + final groupRepo = Provider.of(context, listen: false); + + groupBloc = GroupBloc(repository: groupRepo); + groupBloc.dispatch(GroupInitialized( + groupId: widget.groupId, + group: widget.group, + )); + } + } + + @override + void dispose() { + if (widget.groupBloc == null) groupBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/part_list_profile_widget.dart b/lib/src/presentation/widgets/elements/part_list_profile_widget.dart new file mode 100644 index 0000000..9b52b0c --- /dev/null +++ b/lib/src/presentation/widgets/elements/part_list_profile_widget.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_list_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/part_profile_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/error_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/loading_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_box_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_dialog_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_list_tile_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/utils.dart'; + +/// [SimplePartListProfile] is a dummy widget that use [partIds] or [parts] or +/// [partBlocs] to create a list of [PartProfileWidget] +class SimplePartListProfile extends StatelessWidget { + final List partIds; + final List parts; + final List partBlocs; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + const SimplePartListProfile({ + Key key, + this.partIds, + this.parts, + this.partBlocs, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : assert(partIds != null && parts == null && partBlocs == null), + assert(partIds == null && parts != null && partBlocs == null), + assert(partIds == null && parts == null && partBlocs != null), + super(key: key); + + @override + Widget build(BuildContext context) { + int itemCount; + IndexedWidgetBuilder itemBuilder; + + if (partIds != null && partIds.isNotEmpty) { + itemCount = partIds.length; + itemBuilder = (BuildContext context, int index) => + PartProfileWidget(partId: partIds[index]); + } else if (parts != null && parts.isNotEmpty) { + itemCount = parts.length; + itemBuilder = (BuildContext context, int index) => + PartProfileWidget(part: parts[index]); + } else if (partBlocs != null && partBlocs.isNotEmpty) { + itemCount = partBlocs.length; + itemBuilder = (BuildContext context, int index) => + PartProfileWidget(partBloc: partBlocs[index]); + } + + return ListView.builder( + scrollDirection: scrollDirection, + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + } +} + +/// [ComplexPartListProfile] is a clever widget that use [parentProfileId] or +/// [ownerId] or [partListBloc] to display a list of [PartProfileWidget] +class ComplexPartListProfile extends PartListWidget { + /// Search, filter and sort options + final bool showOptions; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + ComplexPartListProfile({ + Key key, + String parentProfileId, + String ownerId, + PartListBloc partListBloc, + + /// Options + this.showOptions = false, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : super( + key: key, + parentProfileId: parentProfileId, + ownerId: ownerId, + partListBloc: partListBloc, + ); + + @override + State createState() => _PartListProfileState(); +} + +class _PartListProfileState + extends PartListWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: widget.partListBloc, + builder: (BuildContext context, PartListState state) { + if (state is PartListLoading) { + return LoadingList( + count: state.count, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } else if (state is PartListLoaded) { + final List sortItems = [ + SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) + ]; + + final sortRow = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon(Icons.sort_by_alpha), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SortDialog( + title: + Text(CVLocalizations.of(context).partListSorting), + sortItems: sortItems, + ); + }, + ); + }, + ), + DropdownButton( + value: null, + hint: Text(CVLocalizations.of(context).profileListItemPerPage), + items: getDropDownMenuElementPerPage(), + onChanged: (value) {}, + ) + ], + ); + + return ListView( + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + children: [ + if (widget.showOptions) sortRow, + for (var part in state.elements) PartProfileWidget(part: part), + if (widget.showOptions) + Center( + child: FlatButton( + onPressed: null, + child: Text(CVLocalizations.of(context).partListLoadMore), + ), + ), + ], + ); + } else if (state is PartListFailure) { + return ErrorList( + error: state.error, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } + return ErrorList( + error: NotImplementedYetError(), + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/elements/part_list_widget.dart b/lib/src/presentation/widgets/elements/part_list_widget.dart new file mode 100644 index 0000000..6a69fe9 --- /dev/null +++ b/lib/src/presentation/widgets/elements/part_list_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +abstract class PartListWidget extends StatefulWidget { + final String parentProfileId; + final String ownerId; + final PartListBloc partListBloc; + + PartListWidget({ + Key key, + this.parentProfileId, + this.ownerId, + this.partListBloc, + }) : assert( + parentProfileId != null && ownerId == null && partListBloc == null), + assert( + parentProfileId == null && ownerId != null && partListBloc == null), + assert( + parentProfileId == null && ownerId == null && partListBloc != null), + super(key: key); +} + +/// If [widget.partListBloc] exists the lifecycle of it will be managed by its creator +abstract class PartListWidgetState extends State { + PartListBloc partListBloc; + + @override + void initState() { + super.initState(); + + partListBloc = widget.partListBloc; + + if (widget.partListBloc == null) { + final partRepo = Provider.of(context, listen: false); + + partListBloc = PartListBloc(repository: partRepo); + partListBloc.dispatch(PartListInitialized( + parentProfileId: widget.parentProfileId, + ownerId: widget.ownerId, + )); + } + } + + @override + void dispose() { + if (widget.partListBloc == null) partListBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/part_profile_widget.dart b/lib/src/presentation/widgets/elements/part_profile_widget.dart new file mode 100644 index 0000000..74d5517 --- /dev/null +++ b/lib/src/presentation/widgets/elements/part_profile_widget.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [PartProfileWidget] is an [PartWidget] for profile display purpose +class PartProfileWidget extends PartWidget { + PartProfileWidget( + {Key key, String partId, PartEntity part, PartBloc partBloc}) + : super(key: key, partId: partId, part: part, partBloc: partBloc); + + @override + State createState() => _PartProfileWidgetState(); +} + +class _PartProfileWidgetState extends PartWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: partBloc, + builder: (BuildContext context, PartState state) { + if (state is PartLoading) { + return LoadingShadowContent( + numberOfTitleLines: 1, + numberOfContentLines: 2, + ); + } else if (state is PartLoaded) { + return _PartWidgetFromModel(part: state.element); + } else if (state is PartFailure) { + return ErrorRow(error: state.error); + } + return ErrorRow(error: NotImplementedYetError()); + }, + ); + } +} + +class _PartWidgetFromModel extends StatelessWidget { + _PartWidgetFromModel({ + @required this.part, + }) : assert(PartEntity != null); + + final PartEntity part; + + @override + Widget build(BuildContext context) { + if (part.type == kCVPartTypeListHorizontal) { + return _PartWidgetFromModelHorizontal( + partEntity: part, + ); + } else if (part.type == kCVPartTypeListVertical) { + return _PartWidgetFromModelVertical( + part: part, + ); + } else { + return ErrorRow(error: NotImplementedYetError()); + } + } +} + +class _PartWidgetFromModelHorizontal extends StatelessWidget { + _PartWidgetFromModelHorizontal({ + @required this.partEntity, + }) : assert(PartEntity != null); + + final PartEntity partEntity; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + partEntity.name.toUpperCase(), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold), + ), + FlatButton( + child: Text(CVLocalizations.of(context).partWidgetDetails), + onPressed: () => navigateToPart(context, partEntity.id), + ), + ], + ), + Container( + height: AppStyles.groupHorizontalListHeight, + child: SimpleGroupListProfile( + groupIds: partEntity.groupIds, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + ), + ), + ], + ); + } +} + +class _PartWidgetFromModelVertical extends StatelessWidget { + _PartWidgetFromModelVertical({ + @required this.part, + }) : assert(part != null); + + final PartEntity part; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + part.name.toUpperCase(), + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold), + ), + FlatButton( + child: Text(CVLocalizations.of(context).partWidgetDetails), + onPressed: () => navigateToPart(context, part.id), + ), + ], + ), + SimpleGroupListProfile( + groupIds: part.groupIds, + scrollDirection: Axis.vertical, + shrinkWrap: true, + physics: ClampingScrollPhysics(), + ), + ], + ); + } +} diff --git a/lib/src/presentation/widgets/elements/part_widget.dart b/lib/src/presentation/widgets/elements/part_widget.dart new file mode 100644 index 0000000..3c4e8f4 --- /dev/null +++ b/lib/src/presentation/widgets/elements/part_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// If [partBloc] given we assume that it have been already initialized +abstract class PartWidget extends StatefulWidget { + final String partId; + final PartEntity part; + final PartBloc partBloc; + + const PartWidget({Key key, this.partId, this.part, this.partBloc}) + : assert(partId != null && part == null && partBloc == null), + assert(partId == null && part != null && partBloc == null), + assert(partId == null && part == null && partBloc != null), + super(key: key); +} + +/// If [widget.partBloc] exists the lifecycle of it will be managed by its creator +abstract class PartWidgetState extends State { + PartBloc partBloc; + + @override + void initState() { + super.initState(); + + partBloc = widget.partBloc; + + if (partBloc == null) { + final partRepo = Provider.of(context, listen: false); + + partBloc = PartBloc(repository: partRepo); + partBloc.dispatch(PartInitialized( + partId: widget.partId, + part: widget.part, + )); + } + } + + @override + void dispose() { + if (widget.partBloc == null) partBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/profile_list_tile_widget.dart b/lib/src/presentation/widgets/elements/profile_list_tile_widget.dart new file mode 100644 index 0000000..b06aa54 --- /dev/null +++ b/lib/src/presentation/widgets/elements/profile_list_tile_widget.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [SimpleProfileListProfile] is a dummy widget that use [profileIds] or [profiles] or +/// [profileBlocs] to create a list of [ProfileTile] +class SimpleProfileListProfile extends StatelessWidget { + final List profileIds; + final List profiles; + final List profileBlocs; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + const SimpleProfileListProfile({ + Key key, + this.profileIds, + this.profiles, + this.profileBlocs, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : assert(profileIds != null && profiles == null && profileBlocs == null), + assert(profileIds == null && profiles != null && profileBlocs == null), + assert(profileIds == null && profiles == null && profileBlocs != null), + super(key: key); + + @override + Widget build(BuildContext context) { + int itemCount; + IndexedWidgetBuilder itemBuilder; + + if (profileIds != null && profileIds.isNotEmpty) { + itemCount = profileIds.length; + itemBuilder = (BuildContext context, int index) => + ProfileTile(profileId: profileIds[index]); + } else if (profiles != null && profiles.isNotEmpty) { + itemCount = profiles.length; + itemBuilder = (BuildContext context, int index) => + ProfileTile(profile: profiles[index]); + } else if (profileBlocs != null && profileBlocs.isNotEmpty) { + itemCount = profileBlocs.length; + itemBuilder = (BuildContext context, int index) => + ProfileTile(profileBloc: profileBlocs[index]); + } + + return ListView.builder( + scrollDirection: scrollDirection, + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: itemCount, + itemBuilder: itemBuilder, + ); + } +} + +/// [ComplexProfileListProfile] is a clever widget that use [parentProfileId] or +/// [ownerId] or [profileListBloc] to display a list of [ProfileProfileWidget] +class ComplexProfileListProfile extends ProfileListWidget { + /// Search, filter and sort options + final bool showOptions; + + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + ComplexProfileListProfile({ + Key key, + String parentUserId, + String ownerId, + ProfileListBloc profileListBloc, + + /// Options + this.showOptions = false, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : super( + key: key, + parentUserId: parentUserId, + ownerId: ownerId, + profileListBloc: profileListBloc, + ); + + @override + State createState() => _ProfileListProfileState(); +} + +class _ProfileListProfileState + extends ProfileListWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: widget.profileListBloc, + builder: (BuildContext context, ProfileListState state) { + if (state is ProfileListLoading) { + return LoadingList( + count: state.count, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } else if (state is ProfileListLoaded) { + final List sortItems = [ + SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) + ]; + + final sortRow = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon(Icons.sort_by_alpha), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return SortDialog( + title: Text( + CVLocalizations.of(context).profileListSorting), + sortItems: sortItems, + ); + }, + ); + }, + ), + DropdownButton( + value: null, + hint: Text(CVLocalizations.of(context).profileListItemPerPage), + items: getDropDownMenuElementPerPage(), + onChanged: (value) {}, + ) + ], + ); + + return ListView( + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + children: [ + if (widget.showOptions) sortRow, + for (var profile in state.elements) ProfileTile(profile: profile), + if (widget.showOptions) + Center( + child: FlatButton( + onPressed: null, + child: + Text(CVLocalizations.of(context).profileListLoadMore), + ), + ), + ], + ); + } else if (state is ProfileListFailure) { + return ErrorList( + error: state.error, + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + } + return ErrorList( + error: NotImplementedYetError(), + scrollDirection: widget.scrollDirection, + shrinkWrap: widget.shrinkWrap, + physics: widget.physics, + ); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/elements/profile_list_widget.dart b/lib/src/presentation/widgets/elements/profile_list_widget.dart new file mode 100644 index 0000000..4bc8a63 --- /dev/null +++ b/lib/src/presentation/widgets/elements/profile_list_widget.dart @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// If [profileListBloc] given we assume that it have been already initialized +abstract class ProfileListWidget extends StatefulWidget { + final String parentUserId; + final String ownerId; + final ProfileListBloc profileListBloc; + + ProfileListWidget({ + Key key, + this.parentUserId, + this.ownerId, + this.profileListBloc, + }) : assert( + parentUserId != null && ownerId == null && profileListBloc == null), + assert( + parentUserId == null && ownerId != null && profileListBloc == null), + assert( + parentUserId == null && ownerId == null && profileListBloc != null), + super(key: key); +} + +/// If [widget.profileListBloc] exists the lifecycle of it will be managed by its creator +abstract class ProfileListWidgetState + extends State { + ProfileListBloc profileListBloc; + + @override + void initState() { + super.initState(); + profileListBloc = widget.profileListBloc; + if (widget.profileListBloc == null) { + final profileRepo = + Provider.of(context, listen: false); + + profileListBloc = ProfileListBloc(repository: profileRepo); + profileListBloc.dispatch(ProfileListInitialized( + parentUserId: widget.parentUserId, + ownerId: widget.ownerId, + )); + } + } + + @override + void dispose() { + if (widget.profileListBloc == null) profileListBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/elements/profile_tile_widget.dart b/lib/src/presentation/widgets/elements/profile_tile_widget.dart new file mode 100644 index 0000000..0c19345 --- /dev/null +++ b/lib/src/presentation/widgets/elements/profile_tile_widget.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/navigation.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/elements/profile_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/error_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/initial_circle_avatar_widget.dart'; + +class ProfileTile extends ProfileWidget { + ProfileTile({ + Key key, + String profileId, + ProfileEntity profile, + ProfileBloc profileBloc, + }) : super( + key: key, + profileId: profileId, + profile: profile, + profileBloc: profileBloc, + ); + + @override + State createState() => _ProfileTileState(); +} + +class _ProfileTileState extends ProfileWidgetState { + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: widget.profileBloc, + builder: (BuildContext context, ProfileState state) { + if (state is ProfileLoading) { + } else if (state is ProfileLoaded) { + var profile = state.element; + return ListTile( + leading: InitialCircleAvatar( + backgroundImage: NetworkImage('${profile.picture ?? ''}'), + ), + title: Text(profile.title ?? ''), + subtitle: Text(profile.subtitle ?? ''), + onTap: () => navigateToProfile(context, profile.id), + trailing: Icon(MdiIcons.accountDetails), + ); + } else if (state is ProfileFailure) { + return ErrorTile(error: state.error); + } + return ErrorTile(error: NotImplementedYetError()); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/elements/profile_widget.dart b/lib/src/presentation/widgets/elements/profile_widget.dart new file mode 100644 index 0000000..608ac2a --- /dev/null +++ b/lib/src/presentation/widgets/elements/profile_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; + +/// If [profileBloc] given we assume that it have been already initialized +abstract class ProfileWidget extends StatefulWidget { + final String profileId; + final ProfileEntity profile; + final ProfileBloc profileBloc; + + const ProfileWidget({Key key, this.profileId, this.profile, this.profileBloc}) + : assert(profileId != null && profile == null && profileBloc == null), + assert(profileId == null && profile != null && profileBloc == null), + assert(profileId == null && profile == null && profileBloc != null), + super(key: key); +} + +/// If [widget.profileBloc] exists the lifecycle of it will be managed by its creator +abstract class ProfileWidgetState extends State { + ProfileBloc profileBloc; + + @override + void initState() { + super.initState(); + + profileBloc = widget.profileBloc; + + if (profileBloc == null) { + final profileRepo = + Provider.of(context, listen: false); + + profileBloc = ProfileBloc(repository: profileRepo); + profileBloc.dispatch(ProfileInitialized( + profileId: widget.profileId, + profile: widget.profile, + )); + } + } + + @override + void dispose() { + if (widget.profileBloc == null) profileBloc.dispose(); + super.dispose(); + } +} diff --git a/lib/src/presentation/widgets/error_widget.dart b/lib/src/presentation/widgets/error_widget.dart new file mode 100644 index 0000000..d25366c --- /dev/null +++ b/lib/src/presentation/widgets/error_widget.dart @@ -0,0 +1,226 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +/// [CustomErrorWidget] is a based widget for error +/// +/// Must be initialized with an [error] +abstract class CustomErrorWidget extends StatelessWidget { + final dynamic error; + + const CustomErrorWidget({Key key, @required this.error}) + : assert(error != null, 'No error given'), + super(key: key); +} + +/// [ErrorIcon] is a [Icon] like widget to display error +/// +/// See [Icon] widget for more documentation +class ErrorIcon extends StatelessWidget { + final IconData icon; + final double size; + final Color color; + final String semanticLabel; + final TextDirection textDirection; + + const ErrorIcon({ + Key key, + this.icon = MdiIcons.alertCircleOutline, + this.size, + this.color = AppStyles.errorColor, + this.semanticLabel, + this.textDirection, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Icon( + icon, + size: size, + color: color, + semanticLabel: semanticLabel, + textDirection: textDirection, + ); + } +} + +/// [ErrorText] is a [Text] like widget like to display error +/// +/// See [Text] widget for more documentation +class ErrorText extends CustomErrorWidget { + final TextStyle style; + final StrutStyle strutStyle; + + final TextAlign textAlign; + final TextDirection textDirection; + final Locale locale; + final bool softWrap; + final TextOverflow overflow; + final double textScaleFactor; + final int maxLines; + final String semanticsLabel; + + const ErrorText({ + Key key, + @required dynamic error, + this.style, + this.strutStyle, + this.textAlign = TextAlign.center, + this.textDirection, + this.locale, + this.softWrap, + this.overflow, + this.textScaleFactor, + this.maxLines, + this.semanticsLabel, + }) : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return Text( + translateError(context, error), + style: style as TextStyle, + strutStyle: strutStyle as StrutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaleFactor: textScaleFactor, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + ); + } +} + +/// [ErrorRow] is a [Row] like widget to display [Error] +/// +/// See [Row] widget for more documentation +class ErrorRow extends CustomErrorWidget { + const ErrorRow({Key key, @required dynamic error}) + : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const ErrorIcon(), + Expanded(child: ErrorText(error: error)), + ], + ); + } +} + +/// [ErrorTile] is a [ListTile] like widget to display error +/// +/// See [ListTile] widget for more documentation +class ErrorTile extends CustomErrorWidget { + const ErrorTile({Key key, @required dynamic error}) + : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const ErrorIcon(), + title: Text(CVLocalizations.of(context).errorOccurred), + subtitle: ErrorText( + error: error, + textAlign: TextAlign.left, + ), + ); + } +} + +/// [ErrorCard] is a [Card] like widget to display error +/// +/// See [Card] widget for more documentation +class ErrorCard extends CustomErrorWidget { + final double height; + final double width; + + const ErrorCard({ + Key key, + @required dynamic error, + this.height, + this.width, + }) : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return Card( + elevation: AppStyles.cardDefaultElevation, + child: Container( + height: height, + width: width, + padding: AppStyles.cardDefaultPadding, + child: ErrorRow(error: error), + ), + ); + } +} + +/// [ErrorList] is a [ListView] like widget to display error +/// +/// See [ListView] widget for more documentation +class ErrorList extends CustomErrorWidget { + /// List behaviors + final Axis scrollDirection; + final bool shrinkWrap; + final ScrollPhysics physics; + + const ErrorList({ + Key key, + @required dynamic error, + + /// List behaviors + this.scrollDirection = Axis.vertical, + this.shrinkWrap = false, + this.physics, + }) : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return ListView( + scrollDirection: scrollDirection, + shrinkWrap: shrinkWrap, + physics: physics, + children: [ + ErrorTile(error: error), + ], + ); + } +} + +/// [ErrorPage] is a [Scaffold] like widget to display error +/// +/// See [Scaffold] widget for more documentation +class ErrorPage extends CustomErrorWidget { + const ErrorPage({Key key, @required dynamic error}) + : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center(child: ErrorCard(error: error)), + ); + } +} + +/// [ErrorApp] is a [MaterialApp] like widget to display error +/// +/// See [MaterialApp] widget for more documentation +class ErrorApp extends CustomErrorWidget { + const ErrorApp({Key key, @required dynamic error}) + : super(key: key, error: error); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: ErrorPage(error: error), + color: AppStyles.primaryColor, + ); + } +} diff --git a/lib/src/widgets/initial_circle_avatar_widget.dart b/lib/src/presentation/widgets/initial_circle_avatar_widget.dart similarity index 78% rename from lib/src/widgets/initial_circle_avatar_widget.dart rename to lib/src/presentation/widgets/initial_circle_avatar_widget.dart index ff11403..ae8b623 100644 --- a/lib/src/widgets/initial_circle_avatar_widget.dart +++ b/lib/src/presentation/widgets/initial_circle_avatar_widget.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; class InitialCircleAvatar extends StatefulWidget { - InitialCircleAvatar({ + const InitialCircleAvatar({ Key key, this.text = '', this.elevation = 0.0, @@ -21,7 +21,7 @@ class InitialCircleAvatar extends StatefulWidget { final double maxRadius; @override - _InitialCircleAvatarState createState() => new _InitialCircleAvatarState(); + _InitialCircleAvatarState createState() => _InitialCircleAvatarState(); } class _InitialCircleAvatarState extends State { @@ -31,14 +31,14 @@ class _InitialCircleAvatarState extends State { void initState() { super.initState(); widget.backgroundImage - .resolve(new ImageConfiguration()) - .addListener((_, __) { + ?.resolve(const ImageConfiguration()) + ?.addListener(ImageStreamListener((_, __) { if (mounted) { setState(() { _checkLoading = false; }); } - }); + })); } @override @@ -51,7 +51,10 @@ class _InitialCircleAvatarState extends State { minRadius: widget.minRadius, maxRadius: widget.maxRadius, radius: widget.radius, - child: Text(getInitials(widget.text)), + child: Text( + getInitials(widget.text), + textAlign: TextAlign.center, + ), ), ) : Material( diff --git a/lib/src/widgets/loading_widget.dart b/lib/src/presentation/widgets/loading_widget.dart similarity index 72% rename from lib/src/widgets/loading_widget.dart rename to lib/src/presentation/widgets/loading_widget.dart index a518913..7ab4157 100644 --- a/lib/src/widgets/loading_widget.dart +++ b/lib/src/presentation/widgets/loading_widget.dart @@ -2,32 +2,33 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; -/// LoadingCard displays lines with opacity moving up and down +/// [LoadingShadowContent] displays lines with opacity moving up and down /// Specify the number of loading lines to display - class LoadingShadowContent extends StatefulWidget { + final int numberOfTitleLines; + final int numberOfContentLines; + final EdgeInsetsGeometry padding; + const LoadingShadowContent({ Key key, - this.numberOfTitleLines = 1, - this.numberOfContentLines = 3, + this.numberOfTitleLines = 0, + this.numberOfContentLines = 0, this.padding = const EdgeInsets.all(0.0), }) : assert(numberOfTitleLines != null), assert(numberOfContentLines != null), assert(padding != null), super(key: key); - final int numberOfTitleLines; - final int numberOfContentLines; - final EdgeInsetsGeometry padding; - + @override _LoadingShadowContentState createState() => _LoadingShadowContentState(); } class _LoadingShadowContentState extends State with SingleTickerProviderStateMixin { AnimationController _loadingOpacity; - Animation _opacity; + Animation _opacity; Random _rand; double _divideFactor; @@ -36,7 +37,7 @@ class _LoadingShadowContentState extends State @override void initState() { _rand = Random(); - _divideFactor = (_rand.nextInt(5) + 1.4); + _divideFactor = _rand.nextInt(5) + 1.4; _loadingOpacity = AnimationController( vsync: this, duration: Duration(milliseconds: 1500), @@ -51,7 +52,7 @@ class _LoadingShadowContentState extends State @override void dispose() { - _loadingOpacity.dispose(); + _loadingOpacity?.dispose(); super.dispose(); } @@ -69,7 +70,7 @@ class _LoadingShadowContentState extends State @override Widget build(BuildContext context) { - List _widgets = []; + final List _widgets = []; for (int i = 0; i < widget.numberOfTitleLines; i++) { _widgets.add( Align( @@ -79,7 +80,8 @@ class _LoadingShadowContentState extends State child: Container( height: 13.0, width: MediaQuery.of(context).size.width / _divideFactor, - ///constraints: BoxConstraints.expand(), + + //constraints: BoxConstraints.expand(), decoration: BoxDecoration( color: Colors.grey.withOpacity(_opacity.value), borderRadius: BorderRadius.circular(6.5)), @@ -111,6 +113,9 @@ class _LoadingShadowContentState extends State } } +/// [LoadingCard] is a [Card] like widget to display loading state +/// +/// See [Card] widget for more documentation class LoadingCard extends StatelessWidget { const LoadingCard({ Key key, @@ -131,7 +136,7 @@ class LoadingCard extends StatelessWidget { child: Container( height: height, width: width, - padding: EdgeInsets.all(10.0), + padding: AppStyles.cardDefaultPadding, child: LoadingShadowContent( numberOfTitleLines: numberOfTitleLines, numberOfContentLines: numberOfContentLines, @@ -141,7 +146,9 @@ class LoadingCard extends StatelessWidget { } } -/// A widget to list loading entries +/// [LoadingList] is a [ListView] like widget to display loading state +/// +/// See [ListView] widget for more documentation class LoadingList extends StatelessWidget { LoadingList({ @required this.count, @@ -162,7 +169,7 @@ class LoadingList extends StatelessWidget { itemCount: count, itemBuilder: (BuildContext context, int index) { return Card( - child: LoadingShadowContent( + child: const LoadingShadowContent( numberOfTitleLines: 1, numberOfContentLines: 2, ), @@ -171,3 +178,32 @@ class LoadingList extends StatelessWidget { ); } } + +/// [LoadingScaffold] is a [Scaffold] like widget to display loading state +/// +/// See [Scaffold] widget for more documentation +class LoadingScaffold extends StatelessWidget { + const LoadingScaffold({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center(child: const CircularProgressIndicator()), + ); + } +} + +/// [LoadingApp] is a [MaterialApp] like widget to display loading state +/// +/// See [MaterialApp] widget for more documentation +class LoadingApp extends StatelessWidget { + const LoadingApp({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: const LoadingScaffold(), + color: AppStyles.primaryColor, + ); + } +} diff --git a/lib/src/presentation/widgets/login_form_widget.dart b/lib/src/presentation/widgets/login_form_widget.dart new file mode 100644 index 0000000..9b725c0 --- /dev/null +++ b/lib/src/presentation/widgets/login_form_widget.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class LoginForm extends StatefulWidget { + @override + State createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + final String _tag = '$_LoginFormState'; + + final FocusNode myFocusNodeEmailLogin = FocusNode(); + final FocusNode myFocusNodePasswordLogin = FocusNode(); + + TextEditingController loginEmailController = TextEditingController(); + TextEditingController loginPasswordController = TextEditingController(); + + bool _obscureTextLogin = true; + + @override + void dispose() { + print('$_tag:dispose()'); + loginEmailController.dispose(); + loginPasswordController.dispose(); + myFocusNodeEmailLogin.dispose(); + myFocusNodePasswordLogin.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + print('$_tag:build'); + LoginBloc _loginBloc = BlocProvider.of(context); + + void _loginPressed() { + _loginBloc.dispatch(LoginButtonPressed( + email: loginEmailController.text, + password: loginPasswordController.text, + )); + } + + return BlocListener( + bloc: _loginBloc, + listener: (BuildContext context, LoginState state) { + if (state is LoginFailure) { + Scaffold.of(context).showSnackBar( + SnackBar( + backgroundColor: AppStyles.errorColor, + content: ErrorRow(error: state.error), + ), + ); + } + }, + child: BlocBuilder( + bloc: _loginBloc, + builder: (BuildContext context, LoginState state) { + return Card( + elevation: AppStyles.defaultCardElevation, + child: Padding( + padding: AppStyles.defaultCardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Text(CVLocalizations.of(context).authLoginTitle), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + controller: loginEmailController, + decoration: InputDecoration( + hintText: CVLocalizations.of(context).formEmailHint, + labelText: CVLocalizations.of(context).formEmailLabel, + ), + ), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + controller: loginPasswordController, + obscureText: _obscureTextLogin, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(_obscureTextLogin + ? MdiIcons.eyeOffOutline + : MdiIcons.eyeOutline), + onPressed: _toggleLoginPassword, + ), + labelText: + CVLocalizations.of(context).formPasswordLabel, + ), + ), + ), + MaterialButton( + child: Text(CVLocalizations.of(context).authLoginCTA), + onPressed: state is! LoginLoading ? _loginPressed : null, + ), + ], + ), + ), + ); + }, + ), + ); + } + + void _toggleLoginPassword() { + setState(() { + _obscureTextLogin = !_obscureTextLogin; + }); + } +} diff --git a/lib/src/widgets/menu_bottom_sheet_widget.dart b/lib/src/presentation/widgets/menu_bottom_sheet_widget.dart similarity index 67% rename from lib/src/widgets/menu_bottom_sheet_widget.dart rename to lib/src/presentation/widgets/menu_bottom_sheet_widget.dart index 05edd1b..3491291 100644 --- a/lib/src/widgets/menu_bottom_sheet_widget.dart +++ b/lib/src/presentation/widgets/menu_bottom_sheet_widget.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/account_tile_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/theme_switch_tile_widget.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; class MenuBottomSheet extends StatelessWidget { - const MenuBottomSheet({ + final String _tag = '$MenuBottomSheet'; + + MenuBottomSheet({ Key key, this.backgroundColor, this.borderRadius = const BorderRadius.only( - topLeft: const Radius.circular(10.0), - topRight: const Radius.circular(10.0)), + topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)), }) : super(key: key); final Color backgroundColor; @@ -18,14 +16,16 @@ class MenuBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { + Logger.log('$_tag:build'); + return SafeArea( left: false, right: false, child: Wrap( children: [ - AccountTile(), + const AccountTile(), Divider(), - ThemeSwitchTile(), + const ThemeSwitchTile(), ListTile( leading: Icon(Icons.settings), title: Text(CVLocalizations.of(context).settingsCTA), @@ -39,7 +39,7 @@ class MenuBottomSheet extends StatelessWidget { child: Text(CVLocalizations.of(context).menuPPCTA), onPressed: null, ), - Text(CVLocalizations.of(context).middleDot), + const Text('·'), MaterialButton( child: Text(CVLocalizations.of(context).menuToSCTA), onPressed: null, diff --git a/lib/src/presentation/widgets/menu_button_widget.dart b/lib/src/presentation/widgets/menu_button_widget.dart new file mode 100644 index 0000000..0a6d396 --- /dev/null +++ b/lib/src/presentation/widgets/menu_button_widget.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/domain.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class MenuButton extends StatelessWidget { + final String _tag = '$MenuButton'; + final EdgeInsetsGeometry padding; + + MenuButton({ + Key key, + this.padding = const EdgeInsets.symmetric( + vertical: AppStyles.appMenuButtonVerticalPadding, + ), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return Padding( + padding: padding, + child: BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, AuthenticationState state) { + if (state is AuthenticationAuthenticated) + return _MenuButtonConnected(); + if (state is AuthenticationUnauthenticated) + return _MenuButtonNotConnected(); + return ErrorRow(error: NotImplementedYetError()); + }, + ), + ); + } +} + +/// ---------------------------------------------------------------------------- +/// Not connected +/// ---------------------------------------------------------------------------- + +class _MenuButtonNotConnected extends StatelessWidget { + final String _tag = '$_MenuButtonNotConnected'; + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + return IconButton( + onPressed: () => openMenuBottomSheet(context), + icon: Icon(Icons.menu), + ); + } +} + +/// ---------------------------------------------------------------------------- +/// Connected +/// ---------------------------------------------------------------------------- + +class _MenuButtonConnected extends StatelessWidget { + final String _tag = '$_MenuButtonConnected'; + + _MenuButtonConnected({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return BlocBuilder( + bloc: BlocProvider.of(context), + builder: (BuildContext context, IdentityState state) { + if (state is IdentityUninitialized) { + return _MenuButtonNotConnected(); + } else if (state is IdentityLoading) { + return GestureDetector( + onTap: () => openMenuBottomSheet(context), + child: const CircularProgressIndicator(), + ); + } else if (state is IdentityLoaded) { + return IconButton( + onPressed: () => openMenuBottomSheet(context), + icon: InitialCircleAvatar( + text: state.user.username, + backgroundImage: NetworkImage(state.user.picture), + ), + ); + } else if (state is IdentityFailed) { + return IconButton( + onPressed: () => openMenuBottomSheet(context), + icon: const ErrorIcon(), + ); + } + return ErrorText(error: NotImplementedYetError()); + }, + ); + } +} diff --git a/lib/src/presentation/widgets/profile_image_widget.dart b/lib/src/presentation/widgets/profile_image_widget.dart new file mode 100644 index 0000000..cfd7661 --- /dev/null +++ b/lib/src/presentation/widgets/profile_image_widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; + +class ProfileImage extends StatelessWidget { + final String _tag = '$ProfileImage'; + + ProfileImage({Key key, @required this.imageUrl}) + : assert(imageUrl != null), + super(key: key); + + static const RATIO = 1; + + final String imageUrl; + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return Material( + borderRadius: BorderRadius.circular(75.0), + elevation: 2.0, + child: const InitialCircleAvatar(), + ); + } +} diff --git a/lib/src/presentation/widgets/register_form_widget.dart b/lib/src/presentation/widgets/register_form_widget.dart new file mode 100644 index 0000000..6bf0c89 --- /dev/null +++ b/lib/src/presentation/widgets/register_form_widget.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/presentation.dart'; +import 'package:social_cv_client_flutter/src/presentation/commons/styles.dart'; + +class RegisterForm extends StatefulWidget { + @override + State createState() => _RegisterFormState(); +} + +class _RegisterFormState extends State { + final String _tag = '$_RegisterFormState'; + + final FocusNode myFocusNodePassword = FocusNode(); + final FocusNode myFocusNodeEmail = FocusNode(); + + final FocusNode myFocusNodeFirstName = FocusNode(); + final FocusNode myFocusNodeLastName = FocusNode(); + + bool _obscureTextSignUp = true; + bool _obscureTextSignUpConfirm = true; + + TextEditingController signUpEmailController = TextEditingController(); + + TextEditingController signUpFirstNameController = TextEditingController(); + TextEditingController signUpLastNameController = TextEditingController(); + TextEditingController signUpPasswordController = TextEditingController(); + TextEditingController signUpConfirmPasswordController = + TextEditingController(); + + @override + void dispose() { + print('$_tag:dispose()'); + + myFocusNodeFirstName.dispose(); + myFocusNodeLastName.dispose(); + myFocusNodeEmail.dispose(); + myFocusNodePassword.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + print('$_tag:build'); + final RegisterBloc _registerBloc = BlocProvider.of(context); + + void _registerPressed() { + _registerBloc.dispatch(RegistrationEvent( + email: signUpEmailController.text, + password: signUpPasswordController.text, + fName: signUpFirstNameController.text, + lName: signUpLastNameController.text, + )); + } + + return BlocListener( + bloc: _registerBloc, + listener: (BuildContext context, RegisterState state) { + if (state is RegisterFailure) { + Scaffold.of(context).showSnackBar( + SnackBar( + backgroundColor: AppStyles.errorColor, + content: ErrorRow(error: state.error), + ), + ); + } + }, + child: BlocBuilder( + bloc: _registerBloc, + builder: (BuildContext context, RegisterState state) { + return Card( + elevation: AppStyles.defaultCardElevation, + child: Padding( + padding: AppStyles.defaultCardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Text(CVLocalizations.of(context).authRegisterTitle), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + decoration: InputDecoration( + labelText: 'Firstname', + ), + ), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + decoration: InputDecoration( + labelText: 'Lastname', + ), + ), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + controller: signUpEmailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: CVLocalizations.of(context).formEmailHint, + labelText: CVLocalizations.of(context).formEmailLabel, + ), + ), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + controller: signUpPasswordController, + obscureText: _obscureTextSignUp, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(_obscureTextSignUp + ? MdiIcons.eyeOffOutline + : MdiIcons.eyeOutline), + onPressed: _togglePasswordVisibility, + ), + labelText: + CVLocalizations.of(context).formPasswordLabel, + ), + ), + ), + Padding( + padding: AppStyles.defaultFormInputPadding, + child: TextFormField( + controller: signUpConfirmPasswordController, + obscureText: _obscureTextSignUpConfirm, + decoration: InputDecoration( + suffixIcon: IconButton( + icon: Icon(_obscureTextSignUpConfirm + ? MdiIcons.eyeOffOutline + : MdiIcons.eyeOutline), + onPressed: _togglePasswordConfirmationVisibility, + ), + labelText: + CVLocalizations.of(context).formPassword2Label, + ), + ), + ), + MaterialButton( + child: Text(CVLocalizations.of(context).authRegisterCTA), + onPressed: + state is! RegisterLoading ? _registerPressed : null, + ), + ], + ), + ), + ); + }, + ), + ); + } + + void _togglePasswordVisibility() { + setState(() { + _obscureTextSignUp = !_obscureTextSignUp; + }); + } + + void _togglePasswordConfirmationVisibility() { + setState(() { + _obscureTextSignUpConfirm = !_obscureTextSignUpConfirm; + }); + } +} diff --git a/lib/src/presentation/widgets/repository_provider.dart b/lib/src/presentation/widgets/repository_provider.dart new file mode 100644 index 0000000..ba12718 --- /dev/null +++ b/lib/src/presentation/widgets/repository_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class RepositoryProvider extends InheritedWidget { + static Type typeOf() => T; + + final CVRepository repository; + + const RepositoryProvider({ + Key key, + @required Widget child, + @required this.repository, + }) : super(key: key, child: child); + + @override + bool updateShouldNotify(InheritedWidget _) => false; + + static T of(BuildContext context) { + final type = typeOf>(); + return context.inheritFromWidgetOfExactType(type) as T; + } +} diff --git a/lib/src/widgets/sort_box_widget.dart b/lib/src/presentation/widgets/sort_box_widget.dart similarity index 62% rename from lib/src/widgets/sort_box_widget.dart rename to lib/src/presentation/widgets/sort_box_widget.dart index 5c6f081..5a5f665 100644 --- a/lib/src/widgets/sort_box_widget.dart +++ b/lib/src/presentation/widgets/sort_box_widget.dart @@ -7,8 +7,8 @@ enum SortState { NoSort, } -class Sortbox extends StatefulWidget { - const Sortbox({ +class SortBox extends StatelessWidget { + const SortBox({ Key key, this.value, this.onChanged, @@ -18,25 +18,20 @@ class Sortbox extends StatefulWidget { final ValueChanged onChanged; /// The width of a checkbox widget. - static const double width = 18.0; + final double width = 18.0; - @override - _SortboxState createState() => _SortboxState(); -} - -class _SortboxState extends State { @override Widget build(BuildContext context) { IconData iconSort; - if (widget.value == SortState.SortAsc) { + if (value == SortState.SortAsc) { iconSort = Icons.arrow_upward; - } else if (widget.value == SortState.SortDesc) { + } else if (value == SortState.SortDesc) { iconSort = Icons.arrow_downward; } else { iconSort = Icons.close; } return GestureDetector( - onTap: () => widget.onChanged(widget.value), + onTap: () => onChanged(value), child: Icon(iconSort), ); } diff --git a/lib/src/widgets/sort_dialog_widget.dart b/lib/src/presentation/widgets/sort_dialog_widget.dart similarity index 72% rename from lib/src/widgets/sort_dialog_widget.dart rename to lib/src/presentation/widgets/sort_dialog_widget.dart index 7eb6d4f..fd7ac62 100644 --- a/lib/src/widgets/sort_dialog_widget.dart +++ b/lib/src/presentation/widgets/sort_dialog_widget.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_list_tile_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/commons/styles.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/logger.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_box_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_list_tile_widget.dart'; class SortDialog extends StatefulWidget { const SortDialog({ @@ -22,8 +22,12 @@ class SortDialog extends StatefulWidget { } class _SortDialogState extends State { + final String _tag = '$_SortDialogState'; + @override Widget build(BuildContext context) { + Logger.log('$_tag:build'); + final _listTiles = widget.sortItems .map((sortItem) => SortListTile( key: Key(sortItem.field), @@ -31,7 +35,7 @@ class _SortDialogState extends State { title: Text(sortItem.title), onChanged: (SortState value) { setState(() { - logger.info('${sortItem.field} $value'); + Logger.log('${sortItem.field} $value'); sortItem.value = value; }); }, @@ -42,8 +46,8 @@ class _SortDialogState extends State { contentPadding: EdgeInsets.all(0.0), title: widget.title, content: Container( - width: AppDimensions.kCVSortDialogWidth, - height: AppDimensions.kCVSortDialogHeight, + width: AppStyles.sortDialogWidth, + height: AppStyles.sortDialogHeight, child: ReorderableListView( onReorder: _onReorder, children: _listTiles, diff --git a/lib/src/widgets/sort_list_tile_widget.dart b/lib/src/presentation/widgets/sort_list_tile_widget.dart similarity index 88% rename from lib/src/widgets/sort_list_tile_widget.dart rename to lib/src/presentation/widgets/sort_list_tile_widget.dart index 69f01ae..add7ea0 100644 --- a/lib/src/widgets/sort_list_tile_widget.dart +++ b/lib/src/presentation/widgets/sort_list_tile_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; +import 'package:social_cv_client_flutter/src/presentation/widgets/sort_box_widget.dart'; class SortListItem { SortListItem({ @@ -37,7 +37,7 @@ class SortListTile extends StatelessWidget { key: key, leading: Icon(Icons.unfold_more), title: title, - trailing: Sortbox(value: value, onChanged: onChanged), + trailing: SortBox(value: value, onChanged: onChanged), onTap: onChanged != null ? () { if (value == SortState.SortAsc) onChanged(SortState.SortDesc); diff --git a/lib/src/presentation/widgets/splash_widget.dart b/lib/src/presentation/widgets/splash_widget.dart new file mode 100644 index 0000000..6a11bcc --- /dev/null +++ b/lib/src/presentation/widgets/splash_widget.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:social_cv_client_flutter/src/presentation/commons/styles.dart'; +import 'package:social_cv_client_flutter/src/presentation/utils/logger.dart'; + +class SplashPage extends StatelessWidget { + final String _tag = '$SplashPage'; + + @override + Widget build(BuildContext context) { + Logger.log('$_tag:build'); + + return Scaffold( + backgroundColor: AppStyles.primaryColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + Text('Social CV'), + ], + ), + ), + ); + } +} + +class SplashApp extends StatelessWidget { + SplashApp() : super(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: SplashPage(), + color: AppStyles.primaryColor, + ); + } +} diff --git a/lib/src/presentation/widgets/theme_switch_tile_widget.dart b/lib/src/presentation/widgets/theme_switch_tile_widget.dart new file mode 100644 index 0000000..214b5e5 --- /dev/null +++ b/lib/src/presentation/widgets/theme_switch_tile_widget.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:social_cv_client_flutter/bloc.dart'; +import 'package:social_cv_client_flutter/src/presentation/localizations/cv_localization.dart'; + +class ThemeSwitchTile extends StatelessWidget { + const ThemeSwitchTile({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final AppBloc _appBloc = BlocProvider.of(context); + + return BlocBuilder( + bloc: _appBloc, + builder: (BuildContext context, AppState state) { + if (state is AppInitialized) { + final bool darkMode = state.darkMode; + return SwitchListTile( + secondary: Icon( + darkMode ? MdiIcons.weatherSunny : MdiIcons.whiteBalanceSunny, + ), + title: Text(CVLocalizations.of(context).settingsDarkModeCTA), + value: darkMode, + onChanged: (bool newValue) => + _appBloc.dispatch(AppThemeChanged(darkMode: newValue)), + ); + } + return Container(child: Text('${state.runtimeType} state unhandled')); + }, + ); + } +} diff --git a/lib/src/repositories/local_secrets_repository.dart b/lib/src/repositories/local_secrets_repository.dart deleted file mode 100644 index e3b71b9..0000000 --- a/lib/src/repositories/local_secrets_repository.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:async' show Future; -import 'dart:convert' show json; - -import 'package:flutter/services.dart' show rootBundle; -import 'package:social_cv_client_dart_common/repositories.dart'; - -/// From https://medium.com/@sokrato/storing-your-secret-keys-in-flutter-c0b9af1c0f69 - -class LocalSecretsRepository implements SecretsRepository { - static const secretPath = 'secrets.json'; - - @override - Future loadclientId() async { - return (await _load()).clientId; - } - - @override - Future loadclientSecret() async { - return (await _load()).clientSecret; - } - - static Future _load() { - return rootBundle.loadStructuredData( - secretPath, - (jsonStr) async { - final secret = Secret.fromJson(json.decode(jsonStr)); - return secret; - }, - ); - } -} - -/// TODO : Remove 'Secret' class -class Secret { - Secret({ - this.clientId = '', - this.clientSecret = '', - }) : assert(clientId != null), - assert(clientSecret != null); - - final String clientId; - final String clientSecret; - - factory Secret.fromJson(Map jsonMap) { - return new Secret( - clientId: jsonMap['clientId'], - clientSecret: jsonMap['clientSecret'], - ); - } -} diff --git a/lib/src/repositories/repositories_provider.dart b/lib/src/repositories/repositories_provider.dart deleted file mode 100644 index b8ecd14..0000000 --- a/lib/src/repositories/repositories_provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/repositories.dart'; - -class RepositoriesProvider extends InheritedWidget { - const RepositoriesProvider({ - Key key, - Widget child, - this.cvRepository, - this.secretsRepository, - this.preferencesRepository, - }) : super(key: key, child: child); - - final CVRepository cvRepository; - final SecretsRepository secretsRepository; - final PreferencesRepository preferencesRepository; - - @override - bool updateShouldNotify(InheritedWidget oldWidget) => true; - - static RepositoriesProvider of(BuildContext context) { - return context.inheritFromWidgetOfExactType(RepositoriesProvider) - as RepositoriesProvider; - } -} diff --git a/lib/src/repositories/shared_preferences_repository.dart b/lib/src/repositories/shared_preferences_repository.dart deleted file mode 100644 index 9edc4b6..0000000 --- a/lib/src/repositories/shared_preferences_repository.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:social_cv_client_dart_common/repositories.dart'; - -class SharedPreferencesRepository implements PreferencesRepository { - static const String KEY_OAUTH_ACCESS_TOKEN = 'OAUTH_ACCESS_TOKEN'; - static const String KEY_OAUTH_ACCESS_TOKEN_EXPIRATION = - 'OAUTH_ACCESS_TOKEN_EXPIRATION'; - static const String KEY_OAUTH_REFRESH_TOKEN = 'OAUTH_REFRESH_TOKEN'; - static const String KEY_OAUTH_REFRESH_TOKEN_EXPIRATION = - 'OAUTH_REFRESH_TOKEN_EXPIRATION'; - static const String KEY_AUTH_CONNECTED = 'AUTH_CONNECTED'; - static const String KEY_USER_ID = 'USER_ID'; - static const String KEY_APP_THEME = 'APP_THEME'; - - static Future get _prefs => - SharedPreferences.getInstance(); - - /// ---------------------------------------------------------- - /// ------------------------- Tokens ------------------------- - /// ---------------------------------------------------------- - - Future getAccessToken() async { - final SharedPreferences prefs = await _prefs; - return prefs.getString(KEY_OAUTH_ACCESS_TOKEN) ?? ''; - } - - Future setAccessToken(String accessToken) async { - final SharedPreferences prefs = await _prefs; - return prefs.setString(KEY_OAUTH_ACCESS_TOKEN, accessToken); - } - - Future deleteAccessToken() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_OAUTH_ACCESS_TOKEN); - } - - Future getAccessTokenExpiration() async { - final SharedPreferences prefs = await _prefs; - return prefs.getInt(KEY_OAUTH_ACCESS_TOKEN_EXPIRATION) ?? ''; - } - - Future setAccessTokenExpiration(int accessTokenExpiration) async { - final SharedPreferences prefs = await _prefs; - return prefs.setInt( - KEY_OAUTH_ACCESS_TOKEN_EXPIRATION, accessTokenExpiration); - } - - Future deleteAccessTokenExpiration() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_OAUTH_ACCESS_TOKEN_EXPIRATION); - } - - Future getRefreshToken() async { - final SharedPreferences prefs = await _prefs; - return prefs.getString(KEY_OAUTH_REFRESH_TOKEN) ?? ''; - } - - Future setRefreshToken(String refreshToken) async { - final SharedPreferences prefs = await _prefs; - return prefs.setString(KEY_OAUTH_REFRESH_TOKEN, refreshToken); - } - - Future deleteRefreshToken() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_OAUTH_REFRESH_TOKEN); - } - - Future getRefreshTokenExpiration() async { - final SharedPreferences prefs = await _prefs; - return prefs.getString(KEY_OAUTH_REFRESH_TOKEN_EXPIRATION) ?? ''; - } - - Future setRefreshTokenExpiration(String refreshTokenExpiration) async { - final SharedPreferences prefs = await _prefs; - return prefs.setString( - KEY_OAUTH_REFRESH_TOKEN_EXPIRATION, refreshTokenExpiration); - } - - Future deleteRefreshTokenExpiration() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_OAUTH_REFRESH_TOKEN_EXPIRATION); - } - - /// ---------------------------------------------------------- - /// ----------------------- Connected ------------------------ - /// ---------------------------------------------------------- - - Future isAuthConnected() async { - final SharedPreferences prefs = await _prefs; - return prefs.getBool(KEY_AUTH_CONNECTED); - } - - Future setAuthConnected(bool connected) async { - final SharedPreferences prefs = await _prefs; - return prefs.setBool(KEY_AUTH_CONNECTED, connected); - } - - Future deleteAuthConnected() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_AUTH_CONNECTED); - } - - /// ---------------------------------------------------------- - /// ------------------------- User --------------------------- - /// ---------------------------------------------------------- - - Future setUserId(String userId) async { - final SharedPreferences prefs = await _prefs; - return prefs.setString(KEY_USER_ID, userId); - } - - Future getUserId() async { - final SharedPreferences prefs = await _prefs; - return prefs.getString(KEY_USER_ID); - } - - Future deleteUserId() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_USER_ID); - } - - /// ---------------------------------------------------------- - /// ------------------------- Theme -------------------------- - /// ---------------------------------------------------------- - - Future getAppTheme() async { - final SharedPreferences prefs = await _prefs; - return prefs.getString(KEY_APP_THEME); - } - - Future setAppTheme(String theme) async { - final SharedPreferences prefs = await _prefs; - return prefs.setString(KEY_APP_THEME, theme); - } - - Future deleteAppTheme() async { - final SharedPreferences prefs = await _prefs; - return prefs.remove(KEY_APP_THEME); - } - - /// ---------------------------------------------------------- - /// -------------------------- All --------------------------- - /// ---------------------------------------------------------- - - Future deleteAll() async { - await this.deleteAccessToken(); - await this.deleteAccessTokenExpiration(); - await this.deleteRefreshToken(); - await this.deleteRefreshTokenExpiration(); - await this.deleteAuthConnected(); - await this.deleteUserId(); - await this.deleteAppTheme(); - } -} diff --git a/lib/src/routes.dart b/lib/src/routes.dart deleted file mode 100644 index 45f0116..0000000 --- a/lib/src/routes.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:fluro/fluro.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/repositories.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/blocs/main_bloc.dart'; -import 'package:social_cv_client_flutter/src/commons/paths.dart'; -import 'package:social_cv_client_flutter/src/pages/entry_page.dart'; -import 'package:social_cv_client_flutter/src/pages/group_page.dart'; -import 'package:social_cv_client_flutter/src/pages/auth_page.dart'; -import 'package:social_cv_client_flutter/src/pages/part_page.dart'; -import 'package:social_cv_client_flutter/src/pages/profile_page.dart'; -import 'package:social_cv_client_flutter/src/pages/search_page.dart'; -import 'package:social_cv_client_flutter/src/pages/settings_page.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; - -class Routes { - final Router router = Router(); - - Routes({ - this.mainPageProvider, - this.cvRepository, - this.secretsRepository, - this.preferencesRepository, - }) { - _defineRoutes(); - } - - final BlocProvider mainPageProvider; - final CVRepository cvRepository; - final SecretsRepository secretsRepository; - final PreferencesRepository preferencesRepository; - - void _defineRoutes() { - router.define( - AppPaths.kPathHome, - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - logger.info('Navigate to ${AppPaths.kPathHome}'); - return mainPageProvider; - }, - ), - ); - - router.define( - AppPaths.kPathAccount, - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - logger.info('Navigate to ${AppPaths.kPathAccount}'); - return mainPageProvider; - }, - ), - ); - - ///TODO : Check other solution to avoid LoginBloc recreation when - ///LoginPage rebuild (caused by input change) - router.define( - AppPaths.kPathLogin, - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - logger.info('Navigate to ${AppPaths.kPathLogin}'); - return AuthPage(); - }, - ), - ); - - router.define( - AppPaths.kPathSettings, - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - logger.info('Navigate to ${AppPaths.kPathSettings}'); - return SettingsPage(); - }, - ), - ); - - ///TODO : Check other solution to avoid SearchBloc recreation when - ///SearchPage rebuild (caused by input change) - router.define( - AppPaths.kPathSearch, - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - logger.info('Navigate to ${AppPaths.kPathSearch}'); - - return SearchPage(); - }, - ), - ); - - router.define( - '${AppPaths.kPathProfiles}/:${AppPaths.kParamProfileId}', - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - var profileId = params[AppPaths.kParamProfileId][0]; - - logger.info('Navigate to ${AppPaths.kPathProfiles}/$profileId'); - - return BlocProvider( - bloc: ProfileBloc(cvRepository: cvRepository), - child: ProfilePage(profileId: profileId), - ); - }, - ), - ); - - router.define( - '${AppPaths.kPathParts}/:${AppPaths.kParamPartId}', - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - var partId = params[AppPaths.kParamPartId][0]; - - logger.info('Navigate to ${AppPaths.kPathParts}/$partId'); - - return BlocProvider( - bloc: PartBloc(cvRepository: cvRepository), - child: PartPage(partId: partId), - ); - }, - ), - ); - - router.define( - '${AppPaths.kPathGroups}/:${AppPaths.kParamGroupId}', - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - var groupId = params[AppPaths.kParamGroupId][0]; - - logger.info('Navigate to ${AppPaths.kPathGroups}/$groupId'); - - return BlocProvider( - bloc: GroupBloc(cvRepository: cvRepository), - child: GroupPage(groupId: groupId), - ); - }, - ), - ); - - router.define( - '${AppPaths.kPathEntries}/:${AppPaths.kParamEntryId}', - handler: Handler( - handlerFunc: (BuildContext context, Map params) { - var entryId = params[AppPaths.kParamEntryId][0]; - - logger.info('Navigate to ${AppPaths.kPathEntries}/$entryId'); - - return BlocProvider( - bloc: EntryBloc(cvRepository: cvRepository), - child: EntryPage(entryId: entryId), - ); - }, - ), - ); - } -} diff --git a/lib/src/utils/logger.dart b/lib/src/utils/logger.dart deleted file mode 100644 index d4cf5b7..0000000 --- a/lib/src/utils/logger.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; - -Logger logger; - -void initLogger({@required String package, String tag}) { - assert(package != null); - - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((LogRecord rec) { - print('${rec.time}:${rec.loggerName}: ${rec.level.name} -> ${rec.message}'); - }); - - logger = Logger(tag?.toUpperCase() ?? package.toUpperCase()); -} diff --git a/lib/src/utils/logging_service.dart b/lib/src/utils/logging_service.dart deleted file mode 100644 index bc8e73d..0000000 --- a/lib/src/utils/logging_service.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'dart:io'; - -import 'package:meta/meta.dart'; - -//////////////////////////////////////////////////////////////////////////////// -// // -// Helper classes and enums // -// // -//////////////////////////////////////////////////////////////////////////////// - -enum LogType { - log, - info, - warning, - error, - fatal, - action, -} - -enum FatalErrorHandling { - dumpPopup, - dumpPopupEmail, -} - -class ErrorCodes { - static const int UNHANDLED_EXCEPTION = 1; - static const int LOGIN_FAILED = 20; - static const int DOWNLOAD_FAILED = 30; - static const int OPEN_DB_FAILED = 40; -} - -//////////////////////////////////////////////////////////////////////////////// -// // -// Log entry // -// // -//////////////////////////////////////////////////////////////////////////////// - -class LogEntry { - final String message; - final LogType type; - final Duration time; - final String stackTrace; - final int errorCode; - final bool startup; - - LogEntry({ - @required this.type, - @required this.time, - this.message, - this.errorCode, - this.stackTrace, - this.startup = false, - }); - - @override - String toString() { - String msg; - msg = message; - - final err = errorCode != null - ? (errorCode == ErrorCodes.UNHANDLED_EXCEPTION - ? 'Unhandled Exception\n' - : 'Error code: $errorCode. ') - : ''; - final trace = stackTrace != null ? '\n$stackTrace' : ''; - final start = startup ? '[startup]' : ''; - return '[${time.inSeconds}.${time.inMilliseconds % 1000}]$start $err$msg$trace'; - } -} - -//////////////////////////////////////////////////////////////////////////////// -// // -// Logging service // -// // -//////////////////////////////////////////////////////////////////////////////// - -class LoggingService { - static const _LOG_LENGTH = 256; - static const _ACTIONS_LOG_LENGTH = 10; - static final _instance = LoggingService._newInstance(); - - final DateTime _startupTime = DateTime.now(); - List _startupLog = List(); - List _messagesLog = List(_LOG_LENGTH); - List _actionsLog = List(_ACTIONS_LOG_LENGTH); - - File _logFile; - - int nextLogPos = 0; - int nextActionPos = 0; - - static const String INFO = 'info'; - static const String ERROR = 'error'; - - final fatalErrorsHandling = FatalErrorHandling.dumpPopupEmail; - - bool startupPhase = true; - - Duration get runtime { - return DateTime.now().difference(_startupTime); - } - - // ----------------------------------------------------------------------- - // Constructor - // ----------------------------------------------------------------------- - - LoggingService._newInstance() : super(); - - factory LoggingService() { - return _instance; - } - - // ----------------------------------------------------------------------- - // Initialization - // ----------------------------------------------------------------------- - - @override - void initEventHandlers() {} - - /// - /// Init local log file - /// - Future init() async { -// _logFile = await appStorageService.addFile('log.txt', 'log', ''); - } - - // ----------------------------------------------------------------------- - // General log - // ----------------------------------------------------------------------- - - /// - /// Print the message and add a new log entry to the log list - /// - void doLog( - String message, { - LogType type = LogType.log, - int errorCode, - String stackTrace, - }) { - if (message == null || message == '' || message == ' ') { - message = ' ----'; - } - final entry = LogEntry( - type: type, - time: runtime, - message: message, - errorCode: errorCode, - stackTrace: stackTrace, - startup: startupPhase, - ); - if (startupPhase) { - _startupLog.add(entry); - } else { - _messagesLog[nextLogPos] = entry; - nextLogPos = (nextLogPos + 1) % _messagesLog.length; - } - print(entry); - } - - /// - /// Static log function - /// - static void log(String message) => _instance.doLog(message); - - // ----------------------------------------------------------------------- - // Warning - // ----------------------------------------------------------------------- - - /// - /// Do warning log. Use this when something is (probably) wrong - /// but the execution will likely be continued without any serious - /// errors. - /// - void doWarning(String message) { - doLog(message, type: LogType.warning); - } - - /// - /// Static warning function - /// - static void warning(String message) => _instance.doWarning(message); - - // ----------------------------------------------------------------------- - // Info - // ----------------------------------------------------------------------- - - /// - /// Do info log. Use this for general information like startup information. - /// - void doInfo(String message, {String title, int autoCloseSeconds = -1}) { - if (title == null) { - title = INFO; - } - doLog(message); -// return doDialog(message, title, null, null, Dialog.OK, null, null, -// dialogSize, null, autoCloseSeconds); - } - - /// - /// Static info function - /// - static void info(String message, {String title, int autoCloseSeconds = -1}) => - _instance.doInfo(message, title: title); - - // ----------------------------------------------------------------------- - // Error - // ----------------------------------------------------------------------- - - /// - /// Do error log. Something went wrong, the app will not continue the - /// way it is meant to be. - /// - void doError(String message, errorCode, {autoCloseSeconds = -1}) { - final title = '$ERROR ${errorCode != 0 ? errorCode : ''}'; - - doLog(message, type: LogType.error); - -// return doDialog(message, title, null, null, Dialog.OK, null, null, -// dialogSize, null, autoCloseSeconds); - } - - /// - /// Static error function - /// - static void error(String message, {int errorCode}) => - _instance.doError(message, errorCode); - - // ----------------------------------------------------------------------- - // Fatal - // ----------------------------------------------------------------------- - - /// - /// Fatal error. Something went wrong, the app will not continue the - /// way it is meant to be and a mail with error details must be - /// send to us. - /// - void doFatal(String message, int errorCode, {String stackTrace}) { - doLog(message, - type: LogType.fatal, errorCode: errorCode, stackTrace: stackTrace); - - doWarning('Fatal error not handled'); - } - - /// - /// Static fatal function - /// - static void fatal(String message, {int errorCode, String stackTrace}) => - _instance.doFatal(message, errorCode, stackTrace: stackTrace); - - // ----------------------------------------------------------------------- - // Log list - // ----------------------------------------------------------------------- - - static get logList => _instance._logList; - - get _logList { - final List log = _startupLog + _messagesLog + _actionsLog; - log.removeWhere((e) => e == null); - log.sort((a, b) => a.time > b.time ? 1 : -1); - return log; - } - - static get logString => _instance._logString; - - get _logString { - return _logList.join('\n'); - } - -// Future _sendLog() async { -// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); -// AndroidDeviceInfo i = await deviceInfo.androidInfo; -// final version = { -// 'baseOS': i.version.baseOS, -// 'codename': i.version.codename, -// 'incremental': i.version.incremental, -// 'previewSdkInt': i.version.previewSdkInt, -// 'release': i.version.release, -// 'sdkInt': i.version.sdkInt, -// 'securityPatch': i.version.securityPatch, -// }; -// -// final info = { -// 'android id': i.androidId, -// 'isPhysicalDevice': i.isPhysicalDevice, -// 'type': i.type, -// 'version': version, -// 'device': i.device, -// 'brand': i.brand, -// 'manufacturer': i.manufacturer, -// 'model': i.model, -// 'product': i.product, -// 'display': i.display, -// 'fingerprint': i.fingerprint, -// 'hardware': i.hardware, -// 'host': i.host, -// 'id': i.id, -// 'tags': i.tags, -// 'board': i.board, -// 'bootloader': i.bootloader, -// }; -// -// } -} diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart deleted file mode 100644 index 25ad6ee..0000000 --- a/lib/src/utils/navigation.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/commons/paths.dart'; -import 'package:social_cv_client_flutter/src/widgets/menu_bottom_sheet_widget.dart'; - -void navigateToLogin(BuildContext context) { - Navigator.of(context).pushNamed(AppPaths.kPathLogin); -} - -void navigateToSettings(BuildContext context) { - Navigator.of(context).pushNamed(AppPaths.kPathSettings); -} - -void navigateToSearch(BuildContext context) { - Navigator.of(context).pushNamed(AppPaths.kPathSearch); -} - -void navigateToProfile(BuildContext context, String profileId) { - Navigator.of(context) - .pushNamed(AppPaths.kPathProfiles + '/${profileId ?? ''}'); -} - -void navigateToPart(BuildContext context, String partId) { - Navigator.of(context).pushNamed(AppPaths.kPathParts + '/${partId ?? ''}'); -} - -void navigateToGroup(BuildContext context, String groupId) { - Navigator.of(context).pushNamed(AppPaths.kPathGroups + '/${groupId ?? ''}'); -} - -void navigateToEntry(BuildContext context, String entryId) { - Navigator.of(context).pushNamed(AppPaths.kPathEntries + '/${entryId ?? ''}'); -} - -void openMenuBottomSheet(BuildContext context) { - showModalBottomSheet( - context: context, - builder: (context) => MenuBottomSheet(), - ); -} diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart deleted file mode 100644 index 7a31085..0000000 --- a/lib/src/utils/utils.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/errors.dart'; -import 'package:social_cv_client_flutter/src/commons/defaults.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; - -String getInitials(String nameString) { - if (nameString.isEmpty) return ' '; - - List nameArray = - nameString.replaceAll(new RegExp(r'\s+\b|\b\s'), ' ').split(' '); - String initials = ((nameArray[0])[0] != null ? (nameArray[0])[0] : ' ') + - (nameArray.length == 1 ? ' ' : (nameArray[nameArray.length - 1])[0]); - - return initials; -} - -void printException(dynamic e, StackTrace stackTrace, [String message]) { - if (message != null) { - debugPrint('$message: $e'); - } else { - debugPrint(e.toString()); - } -} - -List> getDropDownMenuElementPerPage() { - List _values = [ - kCVItemsPerPage1, - kCVItemsPerPage2, - kCVItemsPerPage3, - kCVItemsPerPage4 - ]; - List> items = new List(); - for (String value in _values) { - items.add(new DropdownMenuItem(value: value, child: new Text(value))); - } - return items; -} - -String translateError(BuildContext context, dynamic err) { - CVLocalizations loc = CVLocalizations.of(context); - logger.info('Translating error'); - - if (err is Exception) { - if (err is FormatException) - return loc.exceptionFormatException; - else if (err is TimeoutException) return loc.exceptionTimeoutException; - } else if (err is BaseError) { - ///Api Errors - if (err is ApiErrorWrongPasswordError) - return loc.apiErrorWrongPasswordError; - - ///HTTP 500 - else if (err is ApiErrorUserNotFoundError) - return loc.apiErrorUserNotFoundError; - - ///HTTP 501 - - ///HTTP Client Errors - if (err is HttpClientErrorBadRequestError) - return loc.httpClientErrorBadRequest; - - ///HTTP 400 - else if (err is HttpClientErrorPaymentRequiredError) - return loc.httpClientErrorPaymentRequired; - - ///HTTP 402 - else if (err is HttpClientErrorForbiddenError) - return loc.httpClientErrorForbidden; - - ///HTTP 403 - else if (err is HttpClientErrorNotFoundError) - return loc.httpClientErrorNotFound; - - ///HTTP 404 - else if (err is HttpClientErrorMethodNotAllowedError) - return loc.httpClientErrorMethodNotAllowed; - - ///HTTP 405 - else if (err is HttpClientErrorNotAcceptableError) - return loc.httpClientErrorNotAcceptable; - - ///HTTP 406 - else if (err is HttpClientErrorRequestTimeoutError) - return loc.httpClientErrorRequestTimeout; - - ///HTTP 408 - else if (err is HttpClientErrorConflictError) - return loc.httpClientErrorConflict; - - ///HTTP 409 - else if (err is HttpClientErrorGoneError) - return loc.httpClientErrorGone; - - ///HTTP 410 - else if (err is HttpClientErrorLengthRequiredError) - return loc.httpClientErrorLengthRequired; - - ///HTTP 411 - else if (err is HttpClientErrorPayloadTooLargeError) - return loc.httpClientErrorPayloadTooLarge; - - ///HTTP 413 - else if (err is HttpClientErrorUriTooLongError) - return loc.httpClientErrorURITooLong; - - ///HTTP 414 - else if (err is HttpClientErrorUnsupportedMediaTypeError) - return loc.httpClientErrorUnsupportedMediaType; - - ///HTTP 415 - else if (err is HttpClientErrorExpectationFailedError) - return loc.httpClientErrorExpectationFailed; - - ///HTTP 417 - else if (err is HttpClientErrorUpgradeRequiredError) - return loc.httpClientErrorUpgradeRequired; - - ///HTTP 426 - - ///HTTP Server Errors - if (err is HttpServerErrorInternalServerError) - return loc.httpServerErrorInternalServerError; - - ///HTTP 500 - else if (err is HttpServerErrorNotImplementedError) - return loc.httpServerErrorNotImplemented; - - ///HTTP 501 - else if (err is HttpServerErrorBadGatewayError) - return loc.httpServerErrorBadGateway; - - ///HTTP 502 - else if (err is HttpServerErrorServiceUnavailableError) - return loc.httpServerErrorServiceUnavailable; - - ///HTTP 503 - else if (err is HttpServerErrorGatewayTimeoutError) - return loc.httpServerErrorGatewayTimeout; - - ///HTTP 504 - else if (err is HttpServerErrorHttpVersionNotSupportedError) - return loc.httpServerErrorHttpVersionNotSupported; - - ///HTTP 505 - } - - ///Default - return err.toString(); -} diff --git a/lib/src/widgets/account_tile_widget.dart b/lib/src/widgets/account_tile_widget.dart deleted file mode 100644 index 798b4c4..0000000 --- a/lib/src/widgets/account_tile_widget.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/initial_circle_avatar_widget.dart'; - -class AccountTile extends StatelessWidget { - const AccountTile({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _accountBloc.isAuthenticatedStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (snapshot.data == true) return _AccountTileConnected(); - if (snapshot.data == false) return _AccountTileNotConnected(); - } - return Container(); - }, - ); - } -} - -class _AccountTileConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _accountBloc.accountDetailsStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - UserModel userModel = snapshot.data; - return ListTile( - leading: InitialCircleAvatar( - text: userModel.username, - backgroundImage: NetworkImage(userModel.picture)), - title: Text(userModel.username), - subtitle: Text(userModel.email), - trailing: IconButton( - icon: Icon(MdiIcons.logout), - onPressed: () => _accountBloc.logout(), - ), - ); - } else if (snapshot.hasError) { - return Container(child: Text('${snapshot.error}')); - } - return Container(); - }, - ); - } -} - -class _AccountTileNotConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - return GestureDetector( - child: ListTile( - title: Center(child: Text(CVLocalizations.of(context).authSignInCTA)), - trailing: Icon(MdiIcons.login), - ), - onTap: () => navigateToLogin(context), - ); - } -} diff --git a/lib/src/widgets/entry_list_widget.dart b/lib/src/widgets/entry_list_widget.dart deleted file mode 100644 index f7666ee..0000000 --- a/lib/src/widgets/entry_list_widget.dart +++ /dev/null @@ -1,228 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/entry_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_dialog_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_list_tile_widget.dart'; - -class EntryListWidget extends StatelessWidget { - const EntryListWidget({ - Key key, - this.fromGroupModel, - this.fromSearch, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(fromGroupModel != null || fromSearch != null), - super(key: key); - - final GroupModel fromGroupModel; - final Object fromSearch; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - if (fromGroupModel != null) { - return _EntryListFromGroup( - groupModel: fromGroupModel, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (fromSearch != null) { - return _EntryListFromSearch( - search: fromSearch, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - return ErrorList( - error: CVLocalizations.of(context).notSupported, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } -} - -class _EntryListFromGroup extends StatelessWidget { - _EntryListFromGroup({ - @required this.groupModel, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(groupModel != null); - - final GroupModel groupModel; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - EntryListBloc entryListBloc = BlocProvider.of(context); - entryListBloc.fetchGroupEntries(groupModel.id); - - return StreamBuilder>( - stream: entryListBloc.entriesStream, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return ErrorList( - error: snapshot.error, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (snapshot.hasData) { - return _EntryList( - entryModels: snapshot.data, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return LoadingList( - count: groupModel.entryIds.length, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - }, - ); - } -} - -class _EntryListFromSearch extends StatelessWidget { - _EntryListFromSearch({ - @required this.search, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(search != null); - - final Object search; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } -} - -class _EntryList extends StatelessWidget { - _EntryList({ - @required this.entryModels, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(entryModels != null); - - final List entryModels; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - EntryListBloc _entryListBloc = BlocProvider.of(context); - - final List sortItems = [ - SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) - ]; - - return ListView.builder( - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - itemCount: showOptions ? entryModels.length + 2 : entryModels.length, - itemBuilder: (BuildContext context, int i) { - if (showOptions) { - if (i == 0) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.sort_by_alpha), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SortDialog( - title: Text( - CVLocalizations.of(context).entryListSorting), - sortItems: sortItems, - ); - }, - ); - }, - ), - StreamBuilder( - stream: _entryListBloc.entryPerPage, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return DropdownButton( - value: snapshot.data, - hint: - Text(CVLocalizations.of(context).partListItemPerPage), - items: getDropDownMenuElementPerPage(), - onChanged: (value) { - _entryListBloc.setItemsPerPage(value); - }, - ); - }, - ), - ], - ); - } - i--; - if (i == entryModels.length) { - return Center( - child: FlatButton( - onPressed: null, - child: Text(CVLocalizations.of(context).entryListLoadMore), - ), - ); - } - } - return EntryWidget(entryModel: entryModels[i]); - }, - ); - } -} diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart deleted file mode 100644 index c207aa1..0000000 --- a/lib/src/widgets/error_widget.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_flutter/src/commons/colors.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; - -class ErrorContent extends StatelessWidget { - ErrorContent({ - @required this.message, - }) : assert(message != null); - - final String message; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Icon( - Icons.error, - color: AppColors.kCVErrorRed, - ), - Expanded( - child: Text(message, textAlign: TextAlign.center), - ) - ], - ); - } -} - -class ErrorCard extends StatelessWidget { - const ErrorCard({ - Key key, - @required this.message, - this.height, - this.width, - }) : assert(message != null), - super(key: key); - - final String message; - final double height; - final double width; - - @override - Widget build(BuildContext context) { - return Card( - child: Container( - height: height, - width: width, - padding: EdgeInsets.all(10.0), - child: ErrorContent(message: message), - ), - ); - } -} - -class ErrorList extends StatelessWidget { - ErrorList({ - @required this.error, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(error != null); - - final Object error; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - return ListView( - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - children: [ - ErrorCard(message: translateError(context, error)), - ], - ); - } -} diff --git a/lib/src/widgets/group_list_widget.dart b/lib/src/widgets/group_list_widget.dart deleted file mode 100644 index 5c6d966..0000000 --- a/lib/src/widgets/group_list_widget.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/group_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_dialog_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_list_tile_widget.dart'; - -/// A widget to list all [GroupModel] from [PartModel] or from a search -class GroupListWidget extends StatelessWidget { - GroupListWidget({ - Key key, - this.fromPartModel, - this.fromSearch, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(fromPartModel != null || fromSearch != null), - super(key: key); - - final PartModel fromPartModel; - final Object fromSearch; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - if (fromPartModel != null) { - return _GroupListFromPartModel( - partModel: fromPartModel, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (fromSearch != null) { - return _GroupListFromSearch( - search: fromSearch, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - } -} - -/// A widget to list all [GroupModel] from [PartModel] -class _GroupListFromPartModel extends StatelessWidget { - _GroupListFromPartModel({ - @required this.partModel, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(partModel != null); - - final PartModel partModel; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - GroupListBloc _groupListBloc = BlocProvider.of(context); - _groupListBloc.fetchPartGroups(partModel.id); - - return StreamBuilder>( - stream: _groupListBloc.groupsStream, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return ErrorList( - error: snapshot.error, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (snapshot.hasData) { - return _GroupList( - groupModels: snapshot.data, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - return LoadingList( - count: partModel.groupIds.length, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - }, - ); - } -} - -/// A widget to list all groups from search -class _GroupListFromSearch extends StatelessWidget { - _GroupListFromSearch({ - @required this.search, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(search != null); - - final Object search; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } -} - -/// A widget to list all [GroupModel] -class _GroupList extends StatelessWidget { - _GroupList({ - @required this.groupModels, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(groupModels != null); - - final List groupModels; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - GroupListBloc _groupListBloc = BlocProvider.of(context); - - final List sortItems = [ - SortListItem(field: 'title', title: 'Title', value: SortState.NoSort) - ]; - - return ListView.builder( - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - itemCount: showOptions ? groupModels.length + 2 : groupModels.length, - itemBuilder: (BuildContext context, int i) { - if (showOptions) { - if (i == 0) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.sort_by_alpha), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SortDialog( - title: - Text(CVLocalizations.of(context).partListSorting), - sortItems: sortItems, - ); - }, - ); - }, - ), - StreamBuilder( - stream: _groupListBloc.groupPerPage, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return DropdownButton( - value: snapshot.data, - hint: - Text(CVLocalizations.of(context).partListItemPerPage), - items: getDropDownMenuElementPerPage(), - onChanged: (value) { - _groupListBloc.setItemsPerPage(value); - }, - ); - }, - ), - ], - ); - } - i--; - if (i == groupModels.length) { - return Center( - child: FlatButton( - onPressed: null, - child: Text(CVLocalizations.of(context).groupListLoadMore), - ), - ); - } - } - return GroupWidget(groupModel: groupModels[i]); - }, - ); - } -} diff --git a/lib/src/widgets/group_widget.dart b/lib/src/widgets/group_widget.dart deleted file mode 100644 index f35bdda..0000000 --- a/lib/src/widgets/group_widget.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/api_values.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/entry_list_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; - -class GroupWidget extends StatelessWidget { - const GroupWidget({ - Key key, - @required this.groupModel, - }) : super(key: key); - - final GroupModel groupModel; - - @override - Widget build(BuildContext context) { - if (groupModel.type == kCVGroupTypeListHorizontal) { - return _GroupHorizontal(groupModel: groupModel); - } else if (groupModel.type == kCVGroupTypeListVertical) { - return _GroupVertical(groupModel: groupModel); - } else { - return ErrorContent(message: CVLocalizations.of(context).notSupported); - } - } -} - -class _GroupHorizontal extends StatelessWidget { - const _GroupHorizontal({ - @required this.groupModel, - }) : assert(groupModel != null); - - final GroupModel groupModel; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppDimensions.kCVGroupPadding), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - groupModel.name.toUpperCase(), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold), - ), - FlatButton( - child: Text(CVLocalizations.of(context).groupWidgetDetails), - onPressed: () => navigateToGroup(context, groupModel.id), - ), - ], - ), - ), - Container( - height: AppDimensions.kCVHorizontalEntryListHeight, - child: BlocProvider( - bloc: EntryListBloc(), - child: EntryListWidget( - fromGroupModel: groupModel, - showOptions: false, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - ), - ), - ), - ], - ); - } -} - -class _GroupVertical extends StatelessWidget { - const _GroupVertical({ - @required this.groupModel, - }) : assert(groupModel != null); - - final GroupModel groupModel; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: AppDimensions.kCVGroupPadding), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - groupModel.name.toUpperCase(), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold), - ), - FlatButton( - child: Text(CVLocalizations.of(context).groupWidgetDetails), - onPressed: () => navigateToGroup(context, groupModel.id), - ), - ], - ), - ), - Card( - elevation: 2.0, - child: BlocProvider( - bloc: EntryListBloc(), - child: EntryListWidget( - fromGroupModel: groupModel, - showOptions: false, - scrollDirection: Axis.vertical, - shrinkWrap: true, - physics: ClampingScrollPhysics(), - ), - ), - ), - ], - ); - } -} diff --git a/lib/src/widgets/login_form_widget.dart b/lib/src/widgets/login_form_widget.dart deleted file mode 100644 index 5f476d0..0000000 --- a/lib/src/widgets/login_form_widget.dart +++ /dev/null @@ -1,306 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/colors.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/logger.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; - -class LoginFormWidget extends StatefulWidget { - const LoginFormWidget({ - Key key, - }) : super(key: key); - - @override - State createState() => _LoginFormWidgetState(); -} - -class _LoginFormWidgetState extends State { - static const _TAG = '_LoginFormWidgetState'; - - _LoginFormWidgetState(); - - final FocusNode myFocusNodeEmailLogin = FocusNode(); - final FocusNode myFocusNodePasswordLogin = FocusNode(); - - TextEditingController loginEmailController = new TextEditingController(); - TextEditingController loginPasswordController = new TextEditingController(); - - bool _obscureTextLogin = true; - - String errorText; - - @override - Widget build(BuildContext context) { - logger.info('$_TAG:build'); - - AccountBloc _accountBloc = BlocProvider.of(context); - - return Container( - padding: EdgeInsets.only(top: 23.0), - child: Column( - children: [ - Stack( - alignment: Alignment.topCenter, - overflow: Overflow.visible, - children: [ - Card( - elevation: 2.0, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: Container( - width: 300.0, - height: 190.0, - child: Column( - children: [ - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - focusNode: myFocusNodeEmailLogin, - controller: loginEmailController, - keyboardType: TextInputType.emailAddress, - style: TextStyle(fontSize: 16.0, color: Colors.black), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.email, - color: Colors.black, - size: 22.0, - ), - hintText: CVLocalizations.of(context).email, - hintStyle: TextStyle(fontSize: 17.0), - ), - ), - ), - Container( - width: 250.0, - height: 1.0, - color: Colors.grey[400], - ), - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - focusNode: myFocusNodePasswordLogin, - controller: loginPasswordController, - obscureText: _obscureTextLogin, - style: TextStyle(fontSize: 16.0, color: Colors.black), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.lock, - size: 22.0, - color: Colors.black, - ), - hintText: CVLocalizations.of(context).password, - hintStyle: TextStyle(fontSize: 17.0), - suffixIcon: GestureDetector( - onTap: _toggleLogin, - child: Icon( - MdiIcons.eye, - size: 15.0, - color: Colors.black, - ), - ), - ), - ), - ), - ], - ), - ), - ), - Container( - margin: EdgeInsets.only(top: 170.0), - decoration: new BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - boxShadow: [ - BoxShadow( - color: AppColors.loginGradientStart, - offset: Offset(1.0, 6.0), - blurRadius: 20.0, - ), - BoxShadow( - color: AppColors.loginGradientEnd, - offset: Offset(1.0, 6.0), - blurRadius: 20.0, - ), - ], - gradient: new LinearGradient( - colors: [ - AppColors.loginGradientEnd, - AppColors.loginGradientStart - ], - begin: const FractionalOffset(0.2, 0.2), - end: const FractionalOffset(1.0, 1.0), - stops: [0.0, 1.0], - tileMode: TileMode.clamp), - ), - child: MaterialButton( - highlightColor: Colors.transparent, - splashColor: AppColors.loginGradientEnd, - //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 42.0), - child: Text( - CVLocalizations.of(context).authSignInCTA, - style: TextStyle( - color: Colors.white, - fontSize: 25.0, - ), - ), - ), - onPressed: () => _accountBloc.login( - loginEmailController.text, loginPasswordController.text), - ), - ), - ], - ), - Padding( - padding: EdgeInsets.only(top: 10.0), - child: FlatButton( - onPressed: () {}, - child: Text( - 'Forgot Password?', - style: TextStyle( - decoration: TextDecoration.underline, - color: Colors.white, - fontSize: 16.0, - ), - )), - ), - Padding( - padding: EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - gradient: new LinearGradient( - colors: [ - Colors.white10, - Colors.white, - ], - begin: const FractionalOffset(0.0, 0.0), - end: const FractionalOffset(1.0, 1.0), - stops: [0.0, 1.0], - tileMode: TileMode.clamp), - ), - width: 100.0, - height: 1.0, - ), - Padding( - padding: EdgeInsets.only(left: 15.0, right: 15.0), - child: Text( - 'Or', - style: TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - ), - Container( - decoration: BoxDecoration( - gradient: new LinearGradient( - colors: [ - Colors.white, - Colors.white10, - ], - begin: const FractionalOffset(0.0, 0.0), - end: const FractionalOffset(1.0, 1.0), - stops: [0.0, 1.0], - tileMode: TileMode.clamp), - ), - width: 100.0, - height: 1.0, - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only(top: 10.0, right: 40.0), - child: GestureDetector( - onTap: () => print('Facebook button pressed'), - child: Container( - padding: const EdgeInsets.all(15.0), - decoration: new BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - ), - child: new Icon( - MdiIcons.facebook, - color: Color(0xFF0084ff), - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only(top: 10.0), - child: GestureDetector( - onTap: () => print('Google button pressed'), - child: Container( - padding: const EdgeInsets.all(15.0), - decoration: new BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - ), - child: new Icon( - MdiIcons.google, - color: Color(0xFF0084ff), - ), - ), - ), - ), - ], - ), - ], - ), - ); - } - - @override - void dispose() { - myFocusNodeEmailLogin.dispose(); - myFocusNodePasswordLogin.dispose(); - super.dispose(); - } - - void _toggleLogin() { - setState(() { - _obscureTextLogin = !_obscureTextLogin; - }); - } -} - -class _LoginFromMessage extends StatelessWidget { - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _accountBloc.accountDetailsStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return ErrorCard(message: translateError(context, snapshot.error)); - } else if (snapshot.hasData) { - return Card( - child: Container( - padding: EdgeInsets.all(10.0), - child: Text('Hello ${snapshot.data.username}'), - ), - ); - } - return Container(); - }, - ); - } -} diff --git a/lib/src/widgets/menu_button_widget.dart b/lib/src/widgets/menu_button_widget.dart deleted file mode 100644 index 632fce8..0000000 --- a/lib/src/widgets/menu_button_widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/initial_circle_avatar_widget.dart'; - -class MenuButton extends StatelessWidget { - const MenuButton({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - - return StreamBuilder( - stream: _accountBloc.isAuthenticatedStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - if (snapshot.data == true) return _MenuButtonConnected(); - if (snapshot.data == false) return _MenuButtonNotConnected(); - } - return Container(); - }, - ); - } -} - -class _MenuButtonNotConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: () => openMenuBottomSheet(context), - icon: Icon(Icons.menu), - ); - } -} - -class _MenuButtonConnected extends StatelessWidget { - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - - return StreamBuilder( - stream: _accountBloc.accountDetailsStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - UserModel userModel = snapshot.data; - return Padding( - padding: EdgeInsets.only(top: 3.0, bottom: 3.0), - child: IconButton( - onPressed: () => openMenuBottomSheet(context), - icon: InitialCircleAvatar( - text: userModel.username, - backgroundImage: NetworkImage(userModel.picture)), - )); - } else if (snapshot.hasError) { - return Container(child: Text('Error ${snapshot.error}')); - } - return Container(); - }, - ); - } -} diff --git a/lib/src/widgets/part_list_widget.dart b/lib/src/widgets/part_list_widget.dart deleted file mode 100644 index 03f364b..0000000 --- a/lib/src/widgets/part_list_widget.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/part_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_dialog_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_list_tile_widget.dart'; - -/// A widget to list all parts from [PartModel] or from a search -class PartListWidget extends StatelessWidget { - PartListWidget({ - Key key, - this.fromProfileModel, - this.fromSearch, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(fromProfileModel != null || fromSearch != null), - super(key: key); - - final ProfileModel fromProfileModel; - final Object fromSearch; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - if (fromProfileModel != null) { - return _PartListFromProfile( - profileModel: fromProfileModel, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (fromSearch != null) { - return _PartListFromSearch( - search: fromSearch, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } -} - -class _PartListFromProfile extends StatelessWidget { - _PartListFromProfile({ - @required this.profileModel, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(profileModel != null); - - final ProfileModel profileModel; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - PartListBloc _partListBloc = BlocProvider.of(context); - _partListBloc.fetchProfileParts(profileModel.id); - - return StreamBuilder>( - stream: _partListBloc.partsStream, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return ErrorList( - error: snapshot.error, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (snapshot.hasData) { - return _PartList( - partModels: snapshot.data, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return LoadingList( - count: profileModel.parts.length, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - }, - ); - } -} - -class _PartListFromSearch extends StatelessWidget { - _PartListFromSearch({ - @required this.search, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(search != null); - - final Object search; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } -} - -class _PartList extends StatelessWidget { - _PartList({ - @required this.partModels, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(partModels != null); - - final List partModels; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - PartListBloc _partListBloc = BlocProvider.of(context); - - final List sortItems = [ - SortListItem(field: 'order', title: 'Order', value: SortState.NoSort), - SortListItem(field: 'name', title: 'Name', value: SortState.NoSort) - ]; - - return ListView.builder( - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - itemCount: showOptions ? partModels.length + 2 : partModels.length, - itemBuilder: (BuildContext context, int i) { - if (showOptions) { - if (i == 0) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.sort_by_alpha), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SortDialog( - title: - Text(CVLocalizations.of(context).partListSorting), - sortItems: sortItems, - ); - }, - ); - }, - ), - StreamBuilder( - stream: _partListBloc.partPerPage, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return DropdownButton( - value: snapshot.data, - hint: - Text(CVLocalizations.of(context).partListItemPerPage), - items: getDropDownMenuElementPerPage(), - onChanged: (value) { - _partListBloc.setItemsPerPage(value); - }, - ); - }, - ), - ], - ); - } - i--; - if (i == partModels.length) { - return Center( - child: FlatButton( - onPressed: null, - child: Text(CVLocalizations.of(context).partListLoadMore), - ), - ); - } - } - return PartWidget(fromPartModel: partModels[i]); - }, - ); - } -} diff --git a/lib/src/widgets/part_widget.dart b/lib/src/widgets/part_widget.dart deleted file mode 100644 index 3b49fb7..0000000 --- a/lib/src/widgets/part_widget.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/api_values.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/group_list_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; - -class PartWidget extends StatelessWidget { - PartWidget({ - Key key, - this.fromPartModel, - this.fromId, - }) : assert(fromPartModel != null || fromId != null), - super(key: key); - - final PartModel fromPartModel; - final String fromId; - - @override - Widget build(BuildContext context) { - if (fromPartModel != null) { - return _PartWidgetFromModel(partModel: fromPartModel); - } else if (fromId != null) { - PartBloc _partBloc = BlocProvider.of(context); - _partBloc.fetchPart(fromId); - - return StreamBuilder( - stream: _partBloc.partStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return ErrorContent( - message: translateError(context, snapshot.error), - ); - } else if (snapshot.hasData) { - return _PartWidgetFromModel(partModel: snapshot.data); - } - return LoadingShadowContent( - numberOfTitleLines: 1, - numberOfContentLines: 2, - ); - }, - ); - } else { - return ErrorContent( - message: CVLocalizations.of(context).notYetImplemented); - } - } -} - -class _PartWidgetFromModel extends StatelessWidget { - _PartWidgetFromModel({ - @required this.partModel, - }) : assert(partModel != null); - - final PartModel partModel; - - @override - Widget build(BuildContext context) { - if (partModel.type == kCVPartTypeListHorizontal) { - return _PartWidgetFromModelHorizontal( - partModel: partModel, - ); - } else if (partModel.type == kCVPartTypeListVertical) { - return _PartWidgetFromModelVertical( - partModel: partModel, - ); - } else { - return ErrorContent(message: CVLocalizations.of(context).notSupported); - } - } -} - -class _PartWidgetFromModelHorizontal extends StatelessWidget { - _PartWidgetFromModelHorizontal({ - @required this.partModel, - }) : assert(partModel != null); - - final PartModel partModel; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - partModel.name.toUpperCase(), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold), - ), - FlatButton( - child: Text(CVLocalizations.of(context).partWidgetDetails), - onPressed: () => navigateToPart(context, partModel.id), - ), - ], - ), - Container( - height: AppDimensions.kCVHorizontalGroupListHeight, - child: BlocProvider( - bloc: GroupListBloc(), - child: GroupListWidget( - fromPartModel: partModel, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - ), - ), - ), - ], - ); - } -} - -class _PartWidgetFromModelVertical extends StatelessWidget { - _PartWidgetFromModelVertical({ - @required this.partModel, - }) : assert(partModel != null); - - final PartModel partModel; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - partModel.name.toUpperCase(), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold), - ), - FlatButton( - child: Text(CVLocalizations.of(context).partWidgetDetails), - onPressed: () => navigateToPart(context, partModel.id), - ), - ], - ), - BlocProvider( - bloc: GroupListBloc(), - child: GroupListWidget( - fromPartModel: partModel, - scrollDirection: Axis.vertical, - shrinkWrap: true, - physics: ClampingScrollPhysics(), - ), - ), - ], - ); - } -} diff --git a/lib/src/widgets/profile_image_widget.dart b/lib/src/widgets/profile_image_widget.dart deleted file mode 100644 index 09c8ed7..0000000 --- a/lib/src/widgets/profile_image_widget.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:social_cv_client_flutter/src/widgets/initial_circle_avatar_widget.dart'; - -class ProfileImage extends StatelessWidget { - const ProfileImage({ - Key key, - @required this.imageUrl, - }) : assert(imageUrl != null), - super(key: key); - - static const RATIO = 1; - - final String imageUrl; - - @override - Widget build(BuildContext context) { - return new Material( - borderRadius: new BorderRadius.circular(75.0), - elevation: 2.0, - child: InitialCircleAvatar(), - ); - } -} diff --git a/lib/src/widgets/profile_list_widget.dart b/lib/src/widgets/profile_list_widget.dart deleted file mode 100644 index 3dd5cef..0000000 --- a/lib/src/widgets/profile_list_widget.dart +++ /dev/null @@ -1,265 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/dimensions.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; -import 'package:social_cv_client_flutter/src/utils/utils.dart'; -import 'package:social_cv_client_flutter/src/widgets/error_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/loading_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/profile_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_box_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_dialog_widget.dart'; -import 'package:social_cv_client_flutter/src/widgets/sort_list_tile_widget.dart'; - -/// A widget to list all profiles from [UserModel] or from a search -class ProfileListWidget extends StatelessWidget { - const ProfileListWidget({ - Key key, - this.fromUserModel, - this.fromSearch, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(fromUserModel != null || fromSearch != null), - super(key: key); - - final UserModel fromUserModel; - final Object fromSearch; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - if (fromUserModel != null) { - return _ProfileListFromUserModel( - fromUserModel, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (fromSearch != null) { - return _ProfileListFromSearch( - search: fromSearch, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return ErrorList( - error: CVLocalizations.of(context).notYetImplemented, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - } -} - -class _ProfileListFromUserModel extends StatelessWidget { - _ProfileListFromUserModel( - this.userModel, { - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }); - - final UserModel userModel; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - ProfileListBloc profileListBloc = BlocProvider.of(context); - - profileListBloc.fetchAccountProfiles(); - - return StreamBuilder>( - stream: profileListBloc.profilesStream, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return ErrorList( - error: snapshot.error, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (snapshot.hasData) { - return _ProfileList( - profileModels: snapshot.data, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return LoadingList( - count: userModel.profileIds.length, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - }, - ); - } -} - -class _ProfileListFromSearch extends StatelessWidget { - _ProfileListFromSearch({ - this.search, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(search != null); - - final Object search; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - ProfileListBloc profileListBloc = BlocProvider.of(context); - profileListBloc.fetchProfiles(search); - - return StreamBuilder>( - stream: profileListBloc.profilesStream, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return ErrorList( - error: snapshot.error, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else if (snapshot.hasData) { - return _ProfileList( - profileModels: snapshot.data, - showOptions: showOptions, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } else { - return LoadingList( - count: 1, - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - ); - } - }, - ); - } -} - -class _ProfileList extends StatelessWidget { - _ProfileList({ - @required this.profileModels, - this.showOptions = false, - this.scrollDirection = Axis.vertical, - this.shrinkWrap = false, - this.physics, - }) : assert(profileModels != null); - - final List profileModels; - - final bool showOptions; - - final Axis scrollDirection; - final bool shrinkWrap; - final ScrollPhysics physics; - - @override - Widget build(BuildContext context) { - ProfileListBloc _profileListBloc = - BlocProvider.of(context); - - final List sortItems = [ - SortListItem(field: 'title', title: 'Title', value: SortState.NoSort) - ]; - - return ListView.builder( - scrollDirection: this.scrollDirection, - shrinkWrap: this.shrinkWrap, - physics: this.physics, - itemCount: showOptions ? profileModels.length + 2 : profileModels.length, - itemBuilder: (BuildContext context, int i) { - if (showOptions) { - if (i == 0) { - return Container( - height: AppDimensions.kCVListHeaderDefaultHeightMax, - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon(Icons.sort_by_alpha), - tooltip: CVLocalizations.of(context).profileListSorting, - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return SortDialog( - title: Text( - CVLocalizations.of(context).profileListSorting), - sortItems: sortItems, - ); - }, - ); - }, - ), - StreamBuilder( - stream: _profileListBloc.profilePerPage, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return DropdownButton( - value: snapshot.data, - hint: Text( - CVLocalizations.of(context).partListItemPerPage), - items: getDropDownMenuElementPerPage(), - onChanged: (value) { - _profileListBloc.setItemsPerPage(value); - }, - ); - }, - ), - ], - ), - ); - } - i--; - if (i == profileModels.length) { - return Center( - child: FlatButton( - onPressed: null, - child: Text(CVLocalizations.of(context).profileListLoadMore), - ), - ); - } - } - return ProfileWidget(profileModel: profileModels[i]); - }, - ); - } -} diff --git a/lib/src/widgets/profile_widget.dart b/lib/src/widgets/profile_widget.dart deleted file mode 100644 index 17138bf..0000000 --- a/lib/src/widgets/profile_widget.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/models.dart'; -import 'package:social_cv_client_flutter/src/utils/navigation.dart'; -import 'package:social_cv_client_flutter/src/widgets/initial_circle_avatar_widget.dart'; - -class ProfileWidget extends StatelessWidget { - const ProfileWidget({ - Key key, - this.profileModel, - }) : assert(profileModel != null), - super(key: key); - - final ProfileModel profileModel; - - @override - Widget build(BuildContext context) { - return ListTile( - leading: InitialCircleAvatar( - backgroundImage: NetworkImage(profileModel.picture ?? ''), - ), - title: Text(profileModel.title ?? ''), - subtitle: Text(profileModel.subtitle ?? ''), - onTap: () => navigateToProfile(context, profileModel.id), - trailing: Icon(MdiIcons.accountDetails), - ); - } -} diff --git a/lib/src/widgets/register_form_widget.dart b/lib/src/widgets/register_form_widget.dart deleted file mode 100644 index 1d50f19..0000000 --- a/lib/src/widgets/register_form_widget.dart +++ /dev/null @@ -1,235 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/commons/colors.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; - -class RegisterFormWidget extends StatefulWidget { - @override - State createState() => _RegisterFormWidgetState(); -} - -class _RegisterFormWidgetState extends State { - final FocusNode myFocusNodePassword = FocusNode(); - final FocusNode myFocusNodeEmail = FocusNode(); - final FocusNode myFocusNodeName = FocusNode(); - - bool _obscureTextSignup = true; - bool _obscureTextSignupConfirm = true; - - TextEditingController signupEmailController = new TextEditingController(); - TextEditingController signupNameController = new TextEditingController(); - TextEditingController signupPasswordController = new TextEditingController(); - TextEditingController signupConfirmPasswordController = - new TextEditingController(); - - @override - Widget build(BuildContext context) { - AccountBloc _accountBloc = BlocProvider.of(context); - - return Container( - padding: EdgeInsets.only(top: 23.0), - child: Column( - children: [ - Stack( - alignment: Alignment.topCenter, - overflow: Overflow.visible, - children: [ - Card( - elevation: 2.0, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - child: Container( - width: 300.0, - height: 360.0, - child: Column( - children: [ - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - focusNode: myFocusNodeName, - controller: signupNameController, - keyboardType: TextInputType.text, - textCapitalization: TextCapitalization.words, - style: TextStyle( - fontSize: 16.0, - color: Colors.black, - ), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.account, - color: Colors.black, - ), - hintText: 'Name', - hintStyle: TextStyle(fontSize: 16.0), - ), - ), - ), - Container( - width: 250.0, - height: 1.0, - color: Colors.grey[400], - ), - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - focusNode: myFocusNodeEmail, - controller: signupEmailController, - keyboardType: TextInputType.emailAddress, - style: TextStyle(fontSize: 16.0, color: Colors.black), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.email, - color: Colors.black, - ), - hintText: CVLocalizations.of(context).email, - hintStyle: TextStyle(fontSize: 16.0), - ), - ), - ), - Container( - width: 250.0, - height: 1.0, - color: Colors.grey[400], - ), - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - focusNode: myFocusNodePassword, - controller: signupPasswordController, - obscureText: _obscureTextSignup, - style: TextStyle(fontSize: 16.0, color: Colors.black), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.lock, - color: Colors.black, - ), - hintText: CVLocalizations.of(context).password, - hintStyle: TextStyle(fontSize: 16.0), - suffixIcon: GestureDetector( - onTap: _toggleSignup, - child: Icon( - MdiIcons.eye, - size: 15.0, - color: Colors.black, - ), - ), - ), - ), - ), - Container( - width: 250.0, - height: 1.0, - color: Colors.grey[400], - ), - Padding( - padding: EdgeInsets.only( - top: 20.0, bottom: 20.0, left: 25.0, right: 25.0), - child: TextField( - controller: signupConfirmPasswordController, - obscureText: _obscureTextSignupConfirm, - style: TextStyle(fontSize: 16.0, color: Colors.black), - decoration: InputDecoration( - border: InputBorder.none, - icon: Icon( - MdiIcons.lock, - color: Colors.black, - ), - hintText: - CVLocalizations.of(context).passwordRepeat, - hintStyle: TextStyle(fontSize: 16.0), - suffixIcon: GestureDetector( - onTap: _toggleSignupConfirm, - child: Icon( - MdiIcons.eye, - size: 15.0, - color: Colors.black, - ), - ), - ), - ), - ), - ], - ), - ), - ), - Container( - margin: EdgeInsets.only(top: 340.0), - decoration: new BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - boxShadow: [ - BoxShadow( - color: AppColors.loginGradientStart, - offset: Offset(1.0, 6.0), - blurRadius: 20.0, - ), - BoxShadow( - color: AppColors.loginGradientEnd, - offset: Offset(1.0, 6.0), - blurRadius: 20.0, - ), - ], - gradient: new LinearGradient( - colors: [ - AppColors.loginGradientEnd, - AppColors.loginGradientStart - ], - begin: const FractionalOffset(0.2, 0.2), - end: const FractionalOffset(1.0, 1.0), - stops: [0.0, 1.0], - tileMode: TileMode.clamp), - ), - child: MaterialButton( - highlightColor: Colors.transparent, - splashColor: AppColors.loginGradientEnd, - //shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, horizontal: 42.0), - child: Text( - CVLocalizations.of(context).authSignUpCTA, - style: TextStyle( - color: Colors.white, - fontSize: 25.0, - ), - ), - ), - onPressed: () {}), - ), - ], - ), - ], - ), - ); - } - - @override - void dispose() { - myFocusNodeName.dispose(); - myFocusNodeEmail.dispose(); - myFocusNodePassword.dispose(); - super.dispose(); - } - - void _toggleSignup() { - setState(() { - _obscureTextSignup = !_obscureTextSignup; - }); - } - - void _toggleSignupConfirm() { - setState(() { - _obscureTextSignupConfirm = !_obscureTextSignupConfirm; - }); - } -} diff --git a/lib/src/widgets/rounded_modal.dart b/lib/src/widgets/rounded_modal.dart deleted file mode 100644 index 9a0fe31..0000000 --- a/lib/src/widgets/rounded_modal.dart +++ /dev/null @@ -1,298 +0,0 @@ -library rounded_modal; - -import 'dart:async'; - -import 'package:flutter/material.dart'; - -/// Adapted from https://gist.github.com/slightfoot/5af4c5dfa52194a3f8577bf83af2e391 - -/// Below is the usage for this function, you'll only have to import this file -/// [radius] takes a double and will be the radius to the rounded corners of this modal -/// [color] will color the modal itself, the default being `Colors.white` -/// [builder] takes the content of the modal, if you're using [Column] -/// or a similar widget, remember to set `mainAxisSize: MainAxisSize.min` -/// so it will only take the needed space. -/// -/// This newer version also fixes the issue of keyboard overlap based on -/// [this gist](https://gist.github.com/slightfoot/5af4c5dfa52194a3f8577bf83af2e391). -/// -/// ```dart -/// showRoundedModalBottomSheet( -/// context: context, -/// radius: 10.0, /// This is the default -/// color: Colors.white, /// Also default -/// builder: (context) => ???, -/// ); -/// ``` -Future showRoundedModalBottomSheet({ - @required BuildContext context, - @required WidgetBuilder builder, - Color color, - double radius: 10.0, - bool autoResize: true, - bool dismissOnTap: true, -}) { - assert(context != null); - assert(builder != null); - assert(radius != null && radius > 0.0); - assert(color != Colors.transparent); - if (color == null) color = Theme.of(context).canvasColor; - - return Navigator.push( - context, - _RoundedCornerModalBottomSheetRoute( - builder: builder, - color: color, - radius: radius, - autoResize: autoResize, - dismissOnTap: dismissOnTap, - barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - ), - ); -} - -const Duration _kRoundedBottomSheetDuration = const Duration(milliseconds: 300); -const double _kMinFlingVelocity = 600.0; -const double _kCloseProgressThreshold = 0.5; - -/// A material design modal bottom sheet. -/// -/// * _Modal_. A modal bottom sheet is an alternative to a menu or a dialog and -/// prevents the user from interacting with the rest of the app. Modal bottom -/// sheets can be created and displayed with the [showRoundedModalBottomSheet] -/// function. -/// -/// The [RoundedBottomSheet] widget itself is rarely used directly. Instead, prefer to -/// create a modal bottom sheet with [showRoundedModalBottomSheet]. -/// -/// See also: -/// -/// * [showRoundedModalBottomSheet] -/// * -class RoundedBottomSheet extends StatefulWidget { - /// Creates a bottom sheet. - /// - /// Typically, bottom sheets are created implicitly by - /// [ScaffoldState.showBottomSheet], for persistent bottom sheets, or by - /// [showModalBottomSheet], for modal bottom sheets. - const RoundedBottomSheet( - {Key key, - this.animationController, - @required this.onClosing, - @required this.builder}) - : assert(onClosing != null), - assert(builder != null), - super(key: key); - - /// The animation that controls the bottom sheet's position. - /// - /// The BottomSheet widget will manipulate the position of this animation, it - /// is not just a passive observer. - final AnimationController animationController; - - /// Called when the bottom sheet begins to close. - /// - /// A bottom sheet might be be prevented from closing (e.g., by user - /// interaction) even after this callback is called. For this reason, this - /// callback might be call multiple times for a given bottom sheet. - final VoidCallback onClosing; - - /// A builder for the contents of the sheet. - /// - /// The bottom sheet will wrap the widget produced by this builder in a - /// [Material] widget. - final WidgetBuilder builder; - - @override - _RoundedBottomSheetState createState() => _RoundedBottomSheetState(); - - /// Creates an animation controller suitable for controlling a [RoundedBottomSheet]. - static AnimationController createAnimationController(TickerProvider vsync) { - return AnimationController( - duration: _kRoundedBottomSheetDuration, - debugLabel: 'RoundedBottomSheet', - vsync: vsync, - ); - } -} - -class _RoundedBottomSheetState extends State { - final GlobalKey _childKey = GlobalKey(debugLabel: 'RoundedBottomSheet child'); - - double get _childHeight { - final RenderBox renderBox = _childKey.currentContext.findRenderObject(); - return renderBox.size.height; - } - - bool get _dismissUnderway => - widget.animationController.status == AnimationStatus.reverse; - - void _handleDragUpdate(DragUpdateDetails details) { - if (_dismissUnderway) return; - widget.animationController.value -= - details.primaryDelta / (_childHeight ?? details.primaryDelta); - } - - void _handleDragEnd(DragEndDetails details) { - if (_dismissUnderway) return; - if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) { - final double flingVelocity = - -details.velocity.pixelsPerSecond.dy / _childHeight; - if (widget.animationController.value > 0.0) - widget.animationController.fling(velocity: flingVelocity); - if (flingVelocity < 0.0) widget.onClosing(); - } else if (widget.animationController.value < _kCloseProgressThreshold) { - if (widget.animationController.value > 0.0) - widget.animationController.fling(velocity: -1.0); - widget.onClosing(); - } else { - widget.animationController.forward(); - } - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onVerticalDragUpdate: _handleDragUpdate, - onVerticalDragEnd: _handleDragEnd, - child: Material( - key: _childKey, - child: widget.builder(context), - ), - ); - } -} - -class _RoundedModalBottomSheetLayout extends SingleChildLayoutDelegate { - _RoundedModalBottomSheetLayout(this.bottomInset, this.progress); - - final double bottomInset; - final double progress; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: constraints.maxHeight * 9.0 / 16.0); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - return Offset(0.0, size.height - bottomInset - childSize.height * progress); - } - - @override - bool shouldRelayout(_RoundedModalBottomSheetLayout oldDelegate) { - return progress != oldDelegate.progress || - bottomInset != oldDelegate.bottomInset; - } -} - -class _RoundedCornerModalBottomSheetRoute extends PopupRoute { - _RoundedCornerModalBottomSheetRoute({ - this.builder, - this.barrierLabel, - this.color, - this.radius, - this.autoResize: false, - this.dismissOnTap: true, - RouteSettings settings, - }) : super(settings: settings); - - final WidgetBuilder builder; - final double radius; - final Color color; - final bool autoResize; - final bool dismissOnTap; - - @override - Duration get transitionDuration => _kRoundedBottomSheetDuration; - - @override - Color get barrierColor => Colors.black54; - - @override - bool get barrierDismissible => true; - - @override - bool get opaque => false; - - @override - bool get maintainState => false; - - @override - String barrierLabel; - - AnimationController animationController; - - @override - AnimationController createAnimationController() { - assert(animationController == null); - animationController = - BottomSheet.createAnimationController(navigator.overlay); - return animationController; - } - - @override - Widget buildPage(BuildContext context, Animation animation, - Animation secondaryAnimation) { - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: Theme( - data: Theme.of(context).copyWith(canvasColor: Colors.transparent), - child: _RoundedModalBottomSheet(route: this), - ), - ); - } -} - -class _RoundedModalBottomSheet extends StatefulWidget { - const _RoundedModalBottomSheet({Key key, this.route}) : super(key: key); - - final _RoundedCornerModalBottomSheetRoute route; - - @override - _RoundedModalBottomSheetState createState() => - _RoundedModalBottomSheetState(); -} - -class _RoundedModalBottomSheetState - extends State<_RoundedModalBottomSheet> { - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: widget.route.dismissOnTap ? () => Navigator.pop(context) : null, - child: AnimatedBuilder( - animation: widget.route.animation, - builder: (context, child) { - return CustomSingleChildLayout( - delegate: _RoundedModalBottomSheetLayout( - widget.route.autoResize - ? MediaQuery.of(context).viewInsets.bottom - : 0.0, - widget.route.animation.value), - child: RoundedBottomSheet( - animationController: widget.route.animationController, - onClosing: () => Navigator.pop(context), - builder: (context) => Container( - decoration: BoxDecoration( - color: widget.route.color, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(widget.route.radius), - topRight: Radius.circular(widget.route.radius), - ), - ), - child: SafeArea( - child: Builder(builder: widget.route.builder), - ), - ), - ), - ); - }, - ), - ); - } -} diff --git a/lib/src/widgets/theme_switch_tile_widget.dart b/lib/src/widgets/theme_switch_tile_widget.dart deleted file mode 100644 index b551e70..0000000 --- a/lib/src/widgets/theme_switch_tile_widget.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:social_cv_client_dart_common/blocs.dart'; -import 'package:social_cv_client_flutter/src/blocs/bloc_provider.dart'; -import 'package:social_cv_client_flutter/src/localizations/cv_localization.dart'; - -class ThemeSwitchTile extends StatelessWidget { - const ThemeSwitchTile({ - Key key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - ApplicationBloc _appBloc = BlocProvider.of(context); - return StreamBuilder( - stream: _appBloc.themeStream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - return SwitchListTile( - secondary: Icon( - snapshot.data == ThemeType.DARK - ? MdiIcons.weatherSunny - : MdiIcons.whiteBalanceSunny, - ), - title: Text(CVLocalizations.of(context).settingsThemeCTA), - value: snapshot.data == ThemeType.DARK ? true : false, - onChanged: (bool enable) { - if (enable) - _appBloc.setTheme(ThemeType.DARK); - else - _appBloc.setTheme(ThemeType.LIGHT); - }); - }, - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 1f425ec..32653f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,5 +1,5 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: analyzer: dependency: transitive @@ -7,105 +7,133 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.35.4" + version: "0.36.4" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.6.0" async: dependency: "direct main" description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.5.0-nullsafety.2" + bloc: + dependency: "direct main" + description: + name: bloc + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.4" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.1.0-nullsafety.2" build: dependency: transitive description: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.2" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.3.2" + version: "0.4.2" build_daemon: dependency: transitive description: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "2.1.4" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.2.1" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.9.0" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "5.1.0" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.3.2" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "6.4.0" + version: "7.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.4" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.0-nullsafety.2" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.2" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.5.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.15.0-nullsafety.4" convert: dependency: transitive description: @@ -113,53 +141,88 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" - cookie_jar: + crypto: dependency: transitive description: - name: cookie_jar + name: crypto url: "https://pub.dartlang.org" source: hosted - version: "0.0.8" - crypto: + version: "2.1.5" + csslib: dependency: transitive description: - name: crypto + name: csslib url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "0.16.2" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.2.4" + version: "1.2.9" dio: dependency: "direct main" description: name: dio url: "https://pub.dartlang.org" source: hosted - version: "1.0.17" + version: "3.0.10" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.10" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-nullsafety.2" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" fixnum: dependency: transitive description: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.9" + version: "0.10.11" fluro: dependency: "direct main" description: name: fluro url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.7.7" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + url: "https://pub.dartlang.org" + source: hosted + version: "0.20.1" flutter_localizations: dependency: "direct main" description: flutter @@ -170,20 +233,25 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" front_end: dependency: transitive description: name: front_end url: "https://pub.dartlang.org" source: hosted - version: "0.1.14" + version: "0.1.19" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.2.0" graphs: dependency: transitive description: @@ -191,111 +259,139 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+4" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+2" + version: "0.12.2" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.2.0" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.4" intl: dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.15.8" + version: "0.16.1" intl_translation: dependency: "direct main" description: name: intl_translation url: "https://pub.dartlang.org" source: hosted - version: "0.17.3" + version: "0.17.10" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.4" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.3-nullsafety.2" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" kernel: dependency: transitive description: name: kernel url: "https://pub.dartlang.org" source: hosted - version: "0.3.14" + version: "0.3.19" logging: - dependency: "direct main" + dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+2" + version: "0.11.4" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.5" + version: "0.12.10-nullsafety.2" material_design_icons_flutter: dependency: "direct main" description: name: material_design_icons_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.7.94" + version: "4.0.5755" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.5" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+2" + version: "0.9.7" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.9.3" package_resolver: dependency: transitive description: @@ -309,21 +405,56 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.8.0-nullsafety.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.9.2" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted + version: "3.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" pool: dependency: transitive description: @@ -331,125 +462,172 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.4" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.5" rxdart: dependency: "direct main" description: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.20.0" + version: "0.22.6" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.5.12+4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+4" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+11" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2+7" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+3" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "0.7.9" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.2+5" + version: "0.2.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - social_cv_client_dart_common: - dependency: "direct main" + source_gen: + dependency: transitive description: - path: "." - ref: HEAD - resolved-ref: "794bb245f287348c34e417f720d0b54f2aabe268" - url: "https://github.com/axellebot/Social-CV-Client-Dart-common" - source: git - version: "1.1.0" + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.4+4" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.8.0-nullsafety.3" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0-nullsafety.5" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.2" stream_transform: dependency: transitive description: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "0.0.17" + version: "1.2.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.1.0-nullsafety.2" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.2" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.2.19-nullsafety.4" timing: dependency: transitive description: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+1" + version: "0.1.1+2" transparent_image: dependency: "direct main" description: @@ -463,35 +641,49 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.4" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0-nullsafety.4" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+10" + version: "0.9.7+15" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.12" + version: "1.1.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.1.15" + version: "2.2.1" sdks: - dart: ">=2.2.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.11.0-0.0 <2.12.0" + flutter: ">=1.17.0 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f8e6bf2..f361e07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,46 +1,55 @@ name: social_cv_client_flutter -description: A new Flutter application. +description: A new CV application. environment: - sdk: '>=2.1.0 <3.0.0' + sdk: ">=2.7.0 <3.0.0" dependencies: - - # Common Social CV Logic - social_cv_client_dart_common: - git: https://github.com/axellebot/Social-CV-Client-Dart-common - version: ^1.0.0 - flutter: sdk: flutter flutter_localizations: sdk: flutter - logging: ^0.11.3+2 + equatable: ^0.1.6 + + # Web Client + http: ^0.12.0 + dio: ^3.0.10 + # Routes fluro: ^1.3.7 + # Async/Rx async: ^2.0.8 - rxdart: ^0.20.0 + rxdart: ^0.22.0 - # HTTP Client - http: ^0.12.0 - dio: ^1.0.13 + # Bloc Pattern + flutter_bloc: ^0.20.0 + bloc: ^0.14.4 + + # Dependency Injection + provider: ^3.0.0 - shared_preferences: ^0.4.3 + # Storage + shared_preferences: ^0.5.12 + + # Serialization + json_annotation: ^2.3.0 # Translations - intl: ^0.15.7 + intl: ^0.16.1 intl_translation: ^0.17.0 + # UI Widgets transparent_image: ^0.1.0 # Icons - material_design_icons_flutter: ^2.7.94 + material_design_icons_flutter: ^4.0.5755 #cupertino_icons: ^0.1.0 dev_dependencies: build_runner: ^1.0.0 + json_serializable: ^2.3.0 flutter_test: sdk: flutter @@ -56,25 +65,25 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/account_card_details-white.png - - assets/images/account_card_details-blue.png - - assets/images/default-avatar.png - - assets/images/default-banner.jpg - - - secrets.json # secret data + - assets/images/account_card_details-white.png + - assets/images/account_card_details-blue.png + - assets/images/default-avatar.png + - assets/images/default-banner.jpg + - assets/images/login_logo.png + - config.json # secret data fonts: - - family: Google Sans - fonts: - - asset: assets/fonts/GoogleSans-Regular.ttf - - asset: assets/fonts/GoogleSans-Medium.ttf - weight: 500 - - asset: assets/fonts/GoogleSans-Bold.ttf - weight: 700 + - family: Google Sans + fonts: + - asset: assets/fonts/GoogleSans-Regular.ttf + - asset: assets/fonts/GoogleSans-Medium.ttf + weight: 500 + - asset: assets/fonts/GoogleSans-Bold.ttf + weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.io/custom-fonts/#from-packages authors: -- Axel LE BOT \ No newline at end of file + - Axel LE BOT \ No newline at end of file diff --git a/secrets.json.dist b/secrets.json.dist deleted file mode 100644 index e417eb8..0000000 --- a/secrets.json.dist +++ /dev/null @@ -1,4 +0,0 @@ -{ - "clientId":"", - "clientSecret":"" -} \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..c172e90 --- /dev/null +++ b/web/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + social_cv_client_flutter + + + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..aa9bddb --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "social_cv_client_flutter", + "short_name": "social_cv_client_flutter", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +}