From 2a39904b3556fe737c4796de6404954e3b65bc4f Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 21 Oct 2022 12:10:00 +0700 Subject: [PATCH] TF-1068 Implement make recent list for input of URL --- .../base/widget/recent_item_tile_widget.dart | 45 +++++++++ .../data/datasource/login_url_datasource.dart | 2 + .../login_url_datasource_impl.dart | 31 ++++++ .../data/model/recent_login_url_cache.dart | 4 +- .../repository/login_url_repository_impl.dart | 5 + .../list_recent_login_url_extension.dart | 12 +++ .../repository/login_url_repository.dart | 2 + ...get_all_recent_login_url_latest_state.dart | 22 +++++ ...recent_login_url_on_mobile_interactor.dart | 22 +++++ .../login/presentation/login_bindings.dart | 3 + .../login/presentation/login_controller.dart | 15 +++ .../login/presentation/login_view.dart | 99 +++++++++++-------- .../login/presentation/login_view_web.dart | 16 ++- lib/main/bindings/local/local_bindings.dart | 2 + 14 files changed, 225 insertions(+), 55 deletions(-) create mode 100644 lib/features/base/widget/recent_item_tile_widget.dart create mode 100644 lib/features/login/domain/extensions/list_recent_login_url_extension.dart create mode 100644 lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart create mode 100644 lib/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart diff --git a/lib/features/base/widget/recent_item_tile_widget.dart b/lib/features/base/widget/recent_item_tile_widget.dart new file mode 100644 index 000000000..03cc0ac1a --- /dev/null +++ b/lib/features/base/widget/recent_item_tile_widget.dart @@ -0,0 +1,45 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; + +class RecentItemTileWidget extends StatelessWidget { + + final T item; + final EdgeInsets? contentPadding; + final ImagePaths imagePath; + + const RecentItemTileWidget(this.item, { + required this.imagePath, + Key? key, + this.contentPadding, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: contentPadding ?? const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset(imagePath.icClockSB), + const SizedBox(width: 8), + Expanded( + child: Text(_getTitle(item), + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black)), + ), + ], + ), + ); + } + + String _getTitle(T item) { + if (item is RecentLoginUrl) { + return item.url; + } + return ''; + } +} \ No newline at end of file diff --git a/lib/features/login/data/datasource/login_url_datasource.dart b/lib/features/login/data/datasource/login_url_datasource.dart index e9d307aea..93deff307 100644 --- a/lib/features/login/data/datasource/login_url_datasource.dart +++ b/lib/features/login/data/datasource/login_url_datasource.dart @@ -2,4 +2,6 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart' abstract class LoginUrlDataSource { Future saveLoginUrl(RecentLoginUrl baseUrl); + + Future> getAllRecentLoginUrlLatest({int? limit, String? pattern}); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart b/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart index 07455fc48..f7a1a40aa 100644 --- a/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart @@ -1,6 +1,7 @@ import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/datasource/login_url_datasource.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/list_recent_login_url_extension.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; class LoginUrlDataSourceImpl implements LoginUrlDataSource { @@ -25,4 +26,34 @@ class LoginUrlDataSourceImpl implements LoginUrlDataSource { throw error; }); } + + @override + Future> getAllRecentLoginUrlLatest({int? limit, String? pattern}) { + return Future.sync(() async { + final listRecentUrlCache = await _recentLoginUrlCacheClient.getAll(); + final listRecentUrl = listRecentUrlCache + .where((recentCache) => _filterRecentUrlCache(recentCache, pattern)) + .map((recentCache) => recentCache.toRecentLoginUrl()) + .toList(); + listRecentUrl.sortByCreationDate(); + + final newLimit = limit ?? 5; + + final newListRecentSUrl = listRecentUrl.length > newLimit + ? listRecentUrl.sublist(0, newLimit) + : listRecentUrl; + + return newListRecentSUrl; + }).catchError((error) { + throw error; + }); + } + + bool _filterRecentUrlCache(RecentLoginUrlCache recentLoginUrlCache, String? pattern) { + if (pattern == null || pattern.trim().isEmpty) { + return true; + } else { + return recentLoginUrlCache.matchUrl(pattern); + } + } } \ No newline at end of file diff --git a/lib/features/login/data/model/recent_login_url_cache.dart b/lib/features/login/data/model/recent_login_url_cache.dart index 3e5057c76..496ebe758 100644 --- a/lib/features/login/data/model/recent_login_url_cache.dart +++ b/lib/features/login/data/model/recent_login_url_cache.dart @@ -31,8 +31,8 @@ extension RecentLoginUrlCacheExtension on RecentLoginUrlCache { return RecentLoginUrl(url, creationDate); } - bool matchUrl(Uri pattern) { - return url.toString().toLowerCase().contains(pattern.toString().toLowerCase()); + bool matchUrl(String pattern) { + return url.toLowerCase().contains(pattern.toLowerCase()); } } diff --git a/lib/features/login/data/repository/login_url_repository_impl.dart b/lib/features/login/data/repository/login_url_repository_impl.dart index 3dfdf9543..9745334df 100644 --- a/lib/features/login/data/repository/login_url_repository_impl.dart +++ b/lib/features/login/data/repository/login_url_repository_impl.dart @@ -11,4 +11,9 @@ class LoginUrlRepositoryImpl implements LoginUrlRepository { Future saveRecentLoginUrl(RecentLoginUrl recentLogin) { return loginInfoDataSource.saveLoginUrl(recentLogin); } + + @override + Future> getAllRecentLoginUrlLatest({int? limit, String? pattern}) { + return loginInfoDataSource.getAllRecentLoginUrlLatest(limit: limit, pattern: pattern); + } } \ No newline at end of file diff --git a/lib/features/login/domain/extensions/list_recent_login_url_extension.dart b/lib/features/login/domain/extensions/list_recent_login_url_extension.dart new file mode 100644 index 000000000..f8772fc4b --- /dev/null +++ b/lib/features/login/domain/extensions/list_recent_login_url_extension.dart @@ -0,0 +1,12 @@ + +import 'package:jmap_dart_client/jmap/core/extensions/date_time_extension.dart'; +import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; + +extension ListRecentLoginUrlExtension on List { + + void sortByCreationDate() { + sort((recentUrl1, recentUrl2) { + return recentUrl1.creationDate.compareToSort(recentUrl2.creationDate, false); + }); + } +} \ No newline at end of file diff --git a/lib/features/login/domain/repository/login_url_repository.dart b/lib/features/login/domain/repository/login_url_repository.dart index 276cb1efa..50c2701c3 100644 --- a/lib/features/login/domain/repository/login_url_repository.dart +++ b/lib/features/login/domain/repository/login_url_repository.dart @@ -2,4 +2,6 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart' abstract class LoginUrlRepository { Future saveRecentLoginUrl(RecentLoginUrl recentLoginUrl); + + Future> getAllRecentLoginUrlLatest({int? limit, String? pattern}); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart b/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart new file mode 100644 index 000000000..3033f4b83 --- /dev/null +++ b/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; + +class GetAllRecentLoginUrlLatestSuccess extends UIState { + + final List listRecentLoginUrl; + + GetAllRecentLoginUrlLatestSuccess(this.listRecentLoginUrl); + + @override + List get props => [listRecentLoginUrl]; +} + +class GetAllRecentLoginUrlLatestFailure extends FeatureFailure { + final dynamic exception; + + GetAllRecentLoginUrlLatestFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart b/lib/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart new file mode 100644 index 000000000..e72152069 --- /dev/null +++ b/lib/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/login_url_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_url_latest_state.dart'; + +class GetAllRecentLoginUrlOnMobileInteractor { + final LoginUrlRepository _loginUrlRepository; + + GetAllRecentLoginUrlOnMobileInteractor(this._loginUrlRepository); + + Future> execute({int? limit, String? pattern}) async { + try{ + final listRecentUrl = await _loginUrlRepository.getAllRecentLoginUrlLatest( + limit: limit, + pattern: pattern); + return Right(GetAllRecentLoginUrlLatestSuccess(listRecentUrl)); + } catch(e) { + return Left(GetAllRecentLoginUrlLatestFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index 2fe3c2710..bf0364a85 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -28,6 +28,7 @@ import 'package:tmail_ui_user/features/login/domain/repository/login_url_reposit import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_configuration_interactor.dart'; @@ -52,6 +53,7 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -110,6 +112,7 @@ class LoginBindings extends BaseBindings { Get.lazyPut(() => SaveLoginUrlOnMobileInteractor( Get.find(), )); + Get.lazyPut(() => GetAllRecentLoginUrlOnMobileInteractor(Get.find())); } @override diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 76c845b1f..2c9fcb794 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -10,6 +10,7 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart' import 'package:tmail_ui_user/features/login/domain/state/authenticate_oidc_on_browser_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_url_latest_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authentication_info_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_oidc_configuration_state.dart'; @@ -17,6 +18,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.d import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_configuration_interactor.dart'; @@ -43,6 +45,7 @@ class LoginController extends BaseController { final GetAuthenticationInfoInteractor _getAuthenticationInfoInteractor; final GetStoredOidcConfigurationInteractor _getStoredOidcConfigurationInteractor; final SaveLoginUrlOnMobileInteractor _saveLoginUrlOnMobileInteractor; + final GetAllRecentLoginUrlOnMobileInteractor _getAllRecentLoginUrlOnMobileInteractor; final TextEditingController urlInputController = TextEditingController(); @@ -60,6 +63,7 @@ class LoginController extends BaseController { this._getAuthenticationInfoInteractor, this._getStoredOidcConfigurationInteractor, this._saveLoginUrlOnMobileInteractor, + this._getAllRecentLoginUrlOnMobileInteractor, ); var loginState = LoginState(Right(LoginInitAction())).obs; @@ -340,6 +344,17 @@ class LoginController extends BaseController { } } + Future> getAllRecentLoginUrlAction(String pattern) async { + return await _getAllRecentLoginUrlOnMobileInteractor + .execute(pattern: pattern) + .then((result) => result.fold( + (failure) => [], + (success) => success is GetAllRecentLoginUrlLatestSuccess + ? success.listRecentLoginUrl + : [] + )); + } + @override void onClose() { urlInputController.clear(); diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index 97962fbbe..81f06be9f 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -1,7 +1,10 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; @@ -18,38 +21,37 @@ class LoginView extends BaseLoginView { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.primaryLightColor, - body: Stack(children: [ - GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + color: Colors.white, child: SafeArea( child: _supportScrollForm(context) - ? Stack( - children: [ - Center(child: SingleChildScrollView(child: _buildCenterForm(context), scrollDirection: Axis.vertical)), - Obx(() { - if (loginController.loginFormType.value == LoginFormType.credentialForm - || loginController.loginFormType.value == LoginFormType.ssoForm) { - return _buildBackButton(context); - } - return const SizedBox.shrink(); - }) - ] - ) - : Stack( - children: [ - _buildCenterForm(context), - Obx(() { - if (loginController.loginFormType.value == LoginFormType.credentialForm - || loginController.loginFormType.value == LoginFormType.ssoForm) { - return _buildBackButton(context); - } - return const SizedBox.shrink(); - }) - ] - ), + ? Stack(children: [ + Center(child: SingleChildScrollView( + child: _buildCenterForm(context), + scrollDirection: Axis.vertical)), + Obx(() { + if (loginController.loginFormType.value == LoginFormType.credentialForm + || loginController.loginFormType.value == LoginFormType.ssoForm) { + return _buildBackButton(context); + } + return const SizedBox.shrink(); + }) + ]) + : Stack(children: [ + _buildCenterForm(context), + Obx(() { + if (loginController.loginFormType.value == LoginFormType.credentialForm + || loginController.loginFormType.value == LoginFormType.ssoForm) { + return _buildBackButton(context); + } + return const SizedBox.shrink(); + }) + ]), ), ), - ])); + )); } Widget _buildCenterForm(BuildContext context) { @@ -130,21 +132,32 @@ class LoginView extends BaseLoginView { Widget _buildUrlInput(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24), - child: Container( - child: (TextFieldBuilder() - ..key(const Key('login_url_input')) - ..onChange((value) => loginController.formatUrl(value)) - ..textInputAction(TextInputAction.next) - ..addController(loginController.urlInputController) - ..keyboardType(TextInputType.url) - ..onSubmitted((value) { - controller.handleNextInUrlInputFormPress(); - }) - ..textDecoration((LoginInputDecorationBuilder() - ..setLabelText(AppLocalizations.of(context).prefix_https) - ..setPrefixText(AppLocalizations.of(context).prefix_https)) - .build())) - .build())); + child: TypeAheadFormField( + textFieldConfiguration: TextFieldConfiguration( + controller: loginController.urlInputController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, + onSubmitted: (value) => controller.handleNextInUrlInputFormPress(), + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).prefix_https) + ..setPrefixText(AppLocalizations.of(context).prefix_https)) + .build() + ), + debounceDuration: const Duration(milliseconds: 300), + suggestionsCallback: (pattern) async { + loginController.formatUrl(pattern); + return loginController.getAllRecentLoginUrlAction(pattern); + }, + itemBuilder: (context, loginUrl) => + RecentItemTileWidget(loginUrl, imagePath: imagePaths), + onSuggestionSelected: (loginUrl) => controller.formatUrl(loginUrl.url), + suggestionsBoxDecoration: const SuggestionsBoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(14))), + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, + )); } Widget _buildExpandedButton(BuildContext context, Widget child) { diff --git a/lib/features/login/presentation/login_view_web.dart b/lib/features/login/presentation/login_view_web.dart index b456cfc55..7a55de9c0 100644 --- a/lib/features/login/presentation/login_view_web.dart +++ b/lib/features/login/presentation/login_view_web.dart @@ -15,16 +15,12 @@ class LoginView extends BaseLoginView { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.primaryLightColor, - body: Stack(children: [ - SafeArea( - child: Center(child: SingleChildScrollView( - child: ResponsiveWidget( - responsiveUtils: responsiveUtils, - mobile: _buildMobileForm(context), - desktop: _buildWebForm(context), - ))) - ), - ]), + body: Center(child: SingleChildScrollView( + child: ResponsiveWidget( + responsiveUtils: responsiveUtils, + mobile: _buildMobileForm(context), + desktop: _buildWebForm(context), + ))), ); } diff --git a/lib/main/bindings/local/local_bindings.dart b/lib/main/bindings/local/local_bindings.dart index 3e81fd5fa..1c9e6dd2d 100644 --- a/lib/main/bindings/local/local_bindings.dart +++ b/lib/main/bindings/local/local_bindings.dart @@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; import 'package:tmail_ui_user/features/caching/encryption_key_cache_client.dart'; import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; import 'package:tmail_ui_user/features/caching/token_oidc_cache_client.dart'; @@ -51,6 +52,7 @@ class LocalBindings extends Bindings { Get.put(AuthenticationInfoCacheManager(Get.find())); Get.put(OidcConfigurationCacheManager(Get.find())); Get.put(LanguageCacheManager(Get.find())); + Get.put(RecentLoginUrlCacheClient()); Get.put(CachingManager( Get.find(), Get.find(),