From 841a49450314d0d051c7ee1a449d87615ade7efd Mon Sep 17 00:00:00 2001 From: Mguy13 Date: Sun, 25 Dec 2022 22:17:59 +0100 Subject: [PATCH] refactoring --- lib/app.dart | 3 +- lib/features/core/abstracts/app_setup.dart | 9 ++- .../core/abstracts/router/app_router.dart | 5 +- .../core/abstracts/router/routes.dart | 2 +- .../core/data/constants/const_colors.dart | 1 + .../core/data/constants/const_values.dart | 2 +- .../core/data/extensions/map_extensions.dart | 2 +- .../extensions/value_notifier_extensions.dart | 2 +- .../core/services/app_lifecycle_service.dart | 22 +++---- .../core/services/connections_service.dart | 4 +- .../core/services/local_storage_service.dart | 10 ++-- .../core/services/logging_service.dart | 1 + .../core/services/navigation_service.dart | 10 +--- .../core/services/overlay_service.dart | 5 +- lib/features/core/utils/mutex.dart | 1 + lib/features/core/views/error_page_view.dart | 4 +- .../core/widgets/animated_column.dart | 59 +++++++++++++++++++ lib/features/home/abstracts/images_api.dart | 1 + .../home/data/enums/search_option.dart | 1 + .../home/data/models/image_model.dart | 1 + .../services/image_cache_manager_service.dart | 15 +++-- .../home/services/images_service.dart | 2 +- .../gallery/downloaded_gallery_view.dart | 27 ++++----- .../home/views/gallery/gallery_view.dart | 50 +++++++++++----- .../views/gallery/gallery_view_model.dart | 2 +- .../views/gallery/search_gallery_view.dart | 14 +---- .../image_carousel/image_carousel_view.dart | 17 +++--- .../image_carousel_view_model.dart | 4 -- lib/features/home/widgets/custom_wrap.dart | 30 ++++++++++ lib/l10n/generated/intl/messages_en.dart | 3 + lib/l10n/generated/l10n.dart | 20 +++++++ lib/l10n/intl_en.arb | 2 + lib/locator.dart | 1 - 33 files changed, 231 insertions(+), 101 deletions(-) create mode 100644 lib/features/core/widgets/animated_column.dart create mode 100644 lib/features/home/widgets/custom_wrap.dart diff --git a/lib/app.dart b/lib/app.dart index 1070a19..914c1e5 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'features/core/abstracts/app_setup.dart'; import 'features/core/abstracts/router/app_router.dart'; import 'features/core/data/constants/const_colors.dart'; +import 'l10n/generated/l10n.dart'; class McgApp extends StatelessWidget { const McgApp({super.key}); @@ -11,7 +12,7 @@ class McgApp extends StatelessWidget { Widget build(BuildContext context) { final appRouter = McgRouter.locate.router; return MaterialApp.router( - title: 'MC Gallery App', + title: Strings.current.appTitle, theme: ConstThemes.materialLightTheme, darkTheme: ConstThemes.materialDarkTheme, supportedLocales: AppSetup.supportedLocales, diff --git a/lib/features/core/abstracts/app_setup.dart b/lib/features/core/abstracts/app_setup.dart index 05b08be..ad33b16 100644 --- a/lib/features/core/abstracts/app_setup.dart +++ b/lib/features/core/abstracts/app_setup.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -49,5 +50,11 @@ abstract class AppSetup { static void Function(Object error, StackTrace stackTrace) get onUncaughtException => ( error, stackTrace, - ) {}; + ) { + log( + 'Error occurred during app startup', + error: error, + stackTrace: stackTrace, + ); + }; } diff --git a/lib/features/core/abstracts/router/app_router.dart b/lib/features/core/abstracts/router/app_router.dart index bcf84a7..2c4d3c5 100644 --- a/lib/features/core/abstracts/router/app_router.dart +++ b/lib/features/core/abstracts/router/app_router.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:mc_gallery/features/home/views/gallery/gallery_view.dart'; -import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart'; +import '/features/home/views/gallery/gallery_view.dart'; +import '/features/home/views/image_carousel/image_carousel_view.dart'; import '../../views/error_page_view.dart'; import 'routes.dart'; @@ -15,7 +15,6 @@ class McgRouter { key: state.pageKey, child: ErrorPageView(error: state.error), ), - //todo(mehul): Add Redirect routes: [ GoRoute( path: Routes.home.routePath, diff --git a/lib/features/core/abstracts/router/routes.dart b/lib/features/core/abstracts/router/routes.dart index d290ca9..751b821 100644 --- a/lib/features/core/abstracts/router/routes.dart +++ b/lib/features/core/abstracts/router/routes.dart @@ -1,6 +1,6 @@ enum Routes { home(RoutesInfo( - routePath: '/gallery', + routePath: '/', routeName: 'Home', )), imageCarousel(RoutesInfo( diff --git a/lib/features/core/data/constants/const_colors.dart b/lib/features/core/data/constants/const_colors.dart index 47465c2..4f69947 100644 --- a/lib/features/core/data/constants/const_colors.dart +++ b/lib/features/core/data/constants/const_colors.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; abstract class ConstColours { /// Smoke Gray => a neutral grey with neutral warmth/cold static const galleryBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0); + static const red = Colors.red; static const white = Colors.white; static const black = Colors.black; diff --git a/lib/features/core/data/constants/const_values.dart b/lib/features/core/data/constants/const_values.dart index a468f1b..c8a9a7d 100644 --- a/lib/features/core/data/constants/const_values.dart +++ b/lib/features/core/data/constants/const_values.dart @@ -3,7 +3,7 @@ abstract class ConstValues { static const String backendHost = 'source.unsplash.com'; static const List backendUrlPathSegments = ['user', 'c_v_r']; - static const int numberOfImages = 20; + static const int numberOfImages = 25; static const int minImageSize = 50; static const int maxImageSize = 100; diff --git a/lib/features/core/data/extensions/map_extensions.dart b/lib/features/core/data/extensions/map_extensions.dart index 74de75a..183e294 100644 --- a/lib/features/core/data/extensions/map_extensions.dart +++ b/lib/features/core/data/extensions/map_extensions.dart @@ -8,7 +8,7 @@ extension MapExtensions on Map { } extension LinkedHashMapExtensions on LinkedHashMap { - /// Updated the value at [valueIndex] to [newValue], in addition to preserving the order. + /// Updates the value at [valueIndex] to [newValue], in addition to preserving the order. void updateValueAt({ required int valueIndex, required B newValue, diff --git a/lib/features/core/data/extensions/value_notifier_extensions.dart b/lib/features/core/data/extensions/value_notifier_extensions.dart index 8d4b278..e38a995 100644 --- a/lib/features/core/data/extensions/value_notifier_extensions.dart +++ b/lib/features/core/data/extensions/value_notifier_extensions.dart @@ -1,4 +1,4 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; extension ValueNotifierBoolExtensions on ValueNotifier { void flipValue() => value = !value; diff --git a/lib/features/core/services/app_lifecycle_service.dart b/lib/features/core/services/app_lifecycle_service.dart index b0dbc71..be55f60 100644 --- a/lib/features/core/services/app_lifecycle_service.dart +++ b/lib/features/core/services/app_lifecycle_service.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:mc_gallery/locator.dart'; +import 'package:flutter/widgets.dart'; +import '/locator.dart'; import 'logging_service.dart'; typedef AddLifeCycleListener = void Function({ @@ -23,7 +23,8 @@ class AppLifecycleService with WidgetsBindingObserver { final LoggingService _loggingService; - late final StreamController _streamController = StreamController.broadcast(); + late final StreamController _lifecycleStateStreamController = + StreamController.broadcast(); final Map _appLifecycleSubscriptions = {}; AppLifecycleState? _appLifeCycleState; @@ -43,14 +44,15 @@ class AppLifecycleService with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { try { _appLifeCycleState = state; - _streamController.add(state); + _lifecycleStateStreamController.add(state); } catch (error, stackTrace) { _loggingService.error( - 'Something went wrong logging ${state.name} inside the didChangeAppLifeCycleState method', + 'Something went wrong with ${state.name} inside the didChangeAppLifeCycleState method', error, stackTrace, ); } + super.didChangeAppLifecycleState(state); } @@ -61,17 +63,17 @@ class AppLifecycleService with WidgetsBindingObserver { }) { try { if (_appLifecycleSubscriptions.containsKey(tag)) { - _loggingService.warning('Tag already active, returning!'); + _loggingService.warning('Tag already active, returning'); } else { final message = 'Adding $tag appLifecycleState listener'; _loggingService.info('$message..'); if (_appLifeCycleState != null && tryCallListenerOnAdd) listener(_appLifeCycleState!); - _appLifecycleSubscriptions[tag] = _streamController.stream.listen(listener); + _appLifecycleSubscriptions[tag] = _lifecycleStateStreamController.stream.listen(listener); _loggingService.good('$message success!'); } } catch (error, stackTrace) { _loggingService.error( - 'Something went wrong adding $tag appLifecycleState listener!', + 'Something went wrong adding $tag appLifecycleState listener', error, stackTrace, ); @@ -88,11 +90,11 @@ class AppLifecycleService with WidgetsBindingObserver { _appLifecycleSubscriptions.remove(tag); _loggingService.good('$message success!'); } else { - _loggingService.warning('Subscription was not found!'); + _loggingService.warning('Subscription was not found'); } } catch (error, stackTrace) { _loggingService.error( - 'Something went wrong removing $tag appLifecycleState listener!', + 'Something went wrong removing $tag appLifecycleState listener', error, stackTrace, ); diff --git a/lib/features/core/services/connections_service.dart b/lib/features/core/services/connections_service.dart index c975563..d22c133 100644 --- a/lib/features/core/services/connections_service.dart +++ b/lib/features/core/services/connections_service.dart @@ -4,10 +4,10 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; -import '/features/core/services/logging_service.dart'; import '/locator.dart'; +import 'logging_service.dart'; -/// Used to observe the current connection type. +/// Used to observe the current connection type and status. class ConnectionsService { ConnectionsService({ required Connectivity connectivity, diff --git a/lib/features/core/services/local_storage_service.dart b/lib/features/core/services/local_storage_service.dart index 0b4898c..3220b68 100644 --- a/lib/features/core/services/local_storage_service.dart +++ b/lib/features/core/services/local_storage_service.dart @@ -1,7 +1,9 @@ import 'package:hive/hive.dart'; -import 'package:mc_gallery/features/core/services/logging_service.dart'; -import 'package:mc_gallery/locator.dart'; +import '/locator.dart'; +import 'logging_service.dart'; + +/// Handles storing state data locally, onto the device itself class LocalStorageService { LocalStorageService() { _init(); @@ -9,10 +11,10 @@ class LocalStorageService { final LoggingService _loggingService = LoggingService.locate; - static const String _userBoxKey = 'userBoxKey'; - late final Box _userBox; + static const String _userBoxKey = 'userBoxKey'; + Future _init() async { _userBox = await Hive.openBox(_userBoxKey); diff --git a/lib/features/core/services/logging_service.dart b/lib/features/core/services/logging_service.dart index 8a968b1..f58f32c 100644 --- a/lib/features/core/services/logging_service.dart +++ b/lib/features/core/services/logging_service.dart @@ -8,6 +8,7 @@ import 'package:talker_dio_logger/talker_dio_logger_settings.dart'; import '/locator.dart'; +/// Handles logging of events. class LoggingService { final Talker _talker = Talker( settings: TalkerSettings( diff --git a/lib/features/core/services/navigation_service.dart b/lib/features/core/services/navigation_service.dart index ec25d1d..a9fc291 100644 --- a/lib/features/core/services/navigation_service.dart +++ b/lib/features/core/services/navigation_service.dart @@ -1,11 +1,12 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart'; -import 'package:mc_gallery/locator.dart'; +import '/features/home/views/image_carousel/image_carousel_view.dart'; +import '/locator.dart'; import '../abstracts/router/app_router.dart'; import '../abstracts/router/routes.dart'; +/// Handles the navigation to and fro all the screens in the app. class NavigationService { const NavigationService({ required McgRouter mcgRouter, @@ -22,10 +23,5 @@ class NavigationService { extra: imageCarouselViewArguments, ); - void backToGallery(BuildContext context) => context.pop(); - - void previous() {} - void next() {} - static NavigationService get locate => Locator.locate(); } diff --git a/lib/features/core/services/overlay_service.dart b/lib/features/core/services/overlay_service.dart index aba06c9..2c59011 100644 --- a/lib/features/core/services/overlay_service.dart +++ b/lib/features/core/services/overlay_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:mc_gallery/features/core/services/logging_service.dart'; -import 'package:mc_gallery/locator.dart'; + +import '/locator.dart'; +import 'logging_service.dart'; class OverlayService { OverlayService({ diff --git a/lib/features/core/utils/mutex.dart b/lib/features/core/utils/mutex.dart index aa8ecce..70be6d3 100644 --- a/lib/features/core/utils/mutex.dart +++ b/lib/features/core/utils/mutex.dart @@ -20,6 +20,7 @@ class Mutex { return value; } + /// Allows listening to the completion status of the last worker process to be released. Future get lastOperationCompletionAwaiter => _completerQueue.isNotEmpty ? _completerQueue.last.future : Future.value(); } diff --git a/lib/features/core/views/error_page_view.dart b/lib/features/core/views/error_page_view.dart index 84aa5fb..8f7041e 100644 --- a/lib/features/core/views/error_page_view.dart +++ b/lib/features/core/views/error_page_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; +import '/l10n/generated/l10n.dart'; import '../widgets/gap.dart'; class ErrorPageView extends StatelessWidget { @@ -15,9 +16,8 @@ class ErrorPageView extends StatelessWidget { return Center( child: Column( children: [ - const Text('Oopsie, there has been an error. Hang tight till we do something about it.'), + Text(Strings.current.errorPageMessage), const Gap(16), - Text('what happened: $error'), ], ), ); diff --git a/lib/features/core/widgets/animated_column.dart b/lib/features/core/widgets/animated_column.dart new file mode 100644 index 0000000..8b3e37e --- /dev/null +++ b/lib/features/core/widgets/animated_column.dart @@ -0,0 +1,59 @@ +import 'package:flutter/widgets.dart'; + +import '/features/core/data/constants/const_durations.dart'; + +/// [AnimatedColumn] Animates its children when they get added or removed at the end +class AnimatedColumn extends StatelessWidget { + const AnimatedColumn({ + required this.children, + this.duration = ConstDurations.oneAndHalfDefaultAnimationDuration, + this.mainAxisAlignment = MainAxisAlignment.start, + this.mainAxisSize = MainAxisSize.max, + this.crossAxisAlignment = CrossAxisAlignment.center, + this.textDirection, + this.verticalDirection = VerticalDirection.down, + this.textBaseline, + this.maxAnimatingChildren = 2, + super.key, + }); + + /// [duration] specifies the duration of the add/remove animation + final Duration duration; + + /// [maxAnimatingChildren] determines the maximum number of chidren that can + /// be animating at once, if more are removed or added at within an animation + /// duration they will pop in instead + final int maxAnimatingChildren; + + final MainAxisAlignment mainAxisAlignment; + final MainAxisSize mainAxisSize; + final CrossAxisAlignment crossAxisAlignment; + final TextDirection? textDirection; + final VerticalDirection verticalDirection; + final TextBaseline? textBaseline; + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: crossAxisAlignment, + children: [ + for (int i = 0; i < children.length + maxAnimatingChildren; i++) + AnimatedSwitcher( + duration: duration, + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1, + child: child, + ), + ), + child: i < children.length ? children[i] : const SizedBox.shrink(), + ), + ], + ); + } +} diff --git a/lib/features/home/abstracts/images_api.dart b/lib/features/home/abstracts/images_api.dart index ecc1e1c..5138887 100644 --- a/lib/features/home/abstracts/images_api.dart +++ b/lib/features/home/abstracts/images_api.dart @@ -7,6 +7,7 @@ import '../data/dtos/image_model_dto.dart'; /// Since I used a site that was more obscure than the ones in the examples, this (otherwise pointless /// and convoluting) interface is for adding a bit of flexibility to change strategy to some other site. abstract class ImagesApi { + /// Returns images fetched through an API as [ImageModelDTO]s. FutureOr> fetchImageUri({required String token}); FutureOr> searchImages({ diff --git a/lib/features/home/data/enums/search_option.dart b/lib/features/home/data/enums/search_option.dart index f95cdf9..96654d5 100644 --- a/lib/features/home/data/enums/search_option.dart +++ b/lib/features/home/data/enums/search_option.dart @@ -1,5 +1,6 @@ import '/l10n/generated/l10n.dart'; +/// Represents an option for specifying a search strategy, for an [ImageModel] enum SearchOption { local, web; diff --git a/lib/features/home/data/models/image_model.dart b/lib/features/home/data/models/image_model.dart index 60ddb65..0b04c17 100644 --- a/lib/features/home/data/models/image_model.dart +++ b/lib/features/home/data/models/image_model.dart @@ -1,5 +1,6 @@ import '../dtos/image_model_dto.dart'; +/// Represents an Image, that would be displayed in the gallery. class ImageModel { const ImageModel({ required this.uri, diff --git a/lib/features/home/services/image_cache_manager_service.dart b/lib/features/home/services/image_cache_manager_service.dart index fcba448..6ded6a7 100644 --- a/lib/features/home/services/image_cache_manager_service.dart +++ b/lib/features/home/services/image_cache_manager_service.dart @@ -1,12 +1,14 @@ import 'dart:ui'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart'; -import 'package:mc_gallery/features/core/services/local_storage_service.dart'; -import 'package:mc_gallery/features/core/services/logging_service.dart'; -import 'package:mc_gallery/locator.dart'; -class ImageCacheManagerService with LoggingService { +import '/features/core/services/app_lifecycle_service.dart'; +import '/features/core/services/local_storage_service.dart'; +import '/features/core/services/logging_service.dart'; +import '/locator.dart'; + +/// Handles maintaining the caching of downloaded images +class ImageCacheManagerService { ImageCacheManagerService( {required AppLifecycleService appLifecycleService, required LocalStorageService localStorageService}) @@ -17,6 +19,7 @@ class ImageCacheManagerService with LoggingService { final AppLifecycleService _appLifecycleService; final LocalStorageService _localStorageService; + final LoggingService _loggingService = LoggingService.locate; final _cacheManager = DefaultCacheManager(); Future emptyCache() async => await _cacheManager.emptyCache(); @@ -31,7 +34,7 @@ class ImageCacheManagerService with LoggingService { case AppLifecycleState.inactive: case AppLifecycleState.paused: case AppLifecycleState.detached: - info('Discarding cached images'); + _loggingService.info('Discarding cached images'); await _cacheManager.emptyCache(); _localStorageService.resetFavourites(); } diff --git a/lib/features/home/services/images_service.dart b/lib/features/home/services/images_service.dart index 212a2ee..dd87e38 100644 --- a/lib/features/home/services/images_service.dart +++ b/lib/features/home/services/images_service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:collection'; import 'package:collection/collection.dart'; -import 'package:mc_gallery/features/home/data/dtos/image_model_dto.dart'; import '/features/core/data/constants/const_sorters.dart'; import '/features/core/data/extensions/iterable_extensions.dart'; @@ -11,6 +10,7 @@ import '/features/core/data/extensions/string_extensions.dart'; import '/features/core/services/local_storage_service.dart'; import '/features/core/services/logging_service.dart'; import '/features/core/utils/mutex.dart'; +import '/features/home/data/dtos/image_model_dto.dart'; import '/locator.dart'; import '../abstracts/images_api.dart'; import '../data/enums/search_option.dart'; diff --git a/lib/features/home/views/gallery/downloaded_gallery_view.dart b/lib/features/home/views/gallery/downloaded_gallery_view.dart index 4247391..631e242 100644 --- a/lib/features/home/views/gallery/downloaded_gallery_view.dart +++ b/lib/features/home/views/gallery/downloaded_gallery_view.dart @@ -18,12 +18,7 @@ class _DownloadedGalleryView extends StatelessWidget { child: ValueListenableBuilder( valueListenable: galleryViewModel.isViewingFavouriteListenable, builder: (context, final isViewingFavourites, _) => !isViewingFavourites - ? Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, + ? CustomWrap( children: [ for (final imageModel in galleryViewModel.imageModels) _StarrableImage( @@ -33,12 +28,7 @@ class _DownloadedGalleryView extends StatelessWidget { ), ], ) - : Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, + : CustomWrap( children: [ for (final favouriteImageModel in galleryViewModel.favouriteImageModels) _StarrableImage( @@ -88,11 +78,14 @@ class _StarrableImageState extends State<_StarrableImage> { context, imageModel: widget.imageModel, ), - child: CachedNetworkImage( - imageUrl: widget.imageModel.uri.toString(), - cacheKey: widget.imageModel.imageIndex.toString(), - progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator( - value: widget.galleryViewModel.downloadProgressValue(progress: progress), + child: Hero( + tag: widget.imageModel.imageIndex, + child: CachedNetworkImage( + imageUrl: widget.imageModel.uri.toString(), + cacheKey: widget.imageModel.imageIndex.toString(), + progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator( + value: widget.galleryViewModel.downloadProgressValue(progress: progress), + ), ), ), ), diff --git a/lib/features/home/views/gallery/gallery_view.dart b/lib/features/home/views/gallery/gallery_view.dart index 211fba5..8faba98 100644 --- a/lib/features/home/views/gallery/gallery_view.dart +++ b/lib/features/home/views/gallery/gallery_view.dart @@ -1,13 +1,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:mc_gallery/features/core/data/constants/const_media.dart'; import '/features/core/data/constants/const_colors.dart'; import '/features/core/data/constants/const_durations.dart'; +import '/features/core/data/constants/const_media.dart'; import '/features/core/widgets/gap.dart'; import '/features/core/widgets/mcg_scaffold.dart'; import '/features/core/widgets/state/multi_value_listenable_builder.dart'; import '/features/core/widgets/state/view_model_builder.dart'; +import '/features/home/widgets/custom_wrap.dart'; import '../../data/enums/search_option.dart'; import '../../data/models/image_model.dart'; import 'gallery_view_model.dart'; @@ -81,21 +82,38 @@ class GalleryView extends StatelessWidget { valueListenable: model.isSearchingListenable, builder: (context, final isSearching, _) => AnimatedSwitcher( duration: ConstDurations.oneAndHalfDefaultAnimationDuration, - child: Column( - children: [ - ValueListenableBuilder( - valueListenable: model.isViewingFavouriteListenable, - builder: (context, final isViewingFavourites, child) => - Switch( - value: isViewingFavourites, - onChanged: model.onFavouriteViewChange, - ), - ), - !isSearching - ? _DownloadedGalleryView(galleryViewModel: model) - : _SearchGalleryView(galleryViewModel: model), - ], - ), + child: !isSearching + ? Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ValueListenableBuilder( + valueListenable: model.isViewingFavouriteListenable, + builder: + (context, final isViewingFavourites, child) => + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ConstMedia.buildIcon( + ConstMedia.favStarOutline, + width: 24, + height: 24, + ), + Switch( + value: isViewingFavourites, + onChanged: model.onFavouriteViewChange, + ), + ConstMedia.buildIcon( + ConstMedia.favStarFilled, + width: 24, + height: 24, + ), + ], + ), + ), + _DownloadedGalleryView(galleryViewModel: model), + ], + ) + : _SearchGalleryView(galleryViewModel: model), ), ); } diff --git a/lib/features/home/views/gallery/gallery_view_model.dart b/lib/features/home/views/gallery/gallery_view_model.dart index f10b515..dac601b 100644 --- a/lib/features/home/views/gallery/gallery_view_model.dart +++ b/lib/features/home/views/gallery/gallery_view_model.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:mc_gallery/features/core/data/extensions/value_notifier_extensions.dart'; import '/features/core/abstracts/base_view_model.dart'; +import '/features/core/data/extensions/value_notifier_extensions.dart'; import '/features/core/services/logging_service.dart'; import '/features/core/services/navigation_service.dart'; import '/locator.dart'; diff --git a/lib/features/home/views/gallery/search_gallery_view.dart b/lib/features/home/views/gallery/search_gallery_view.dart index 7ec68b3..87b8df7 100644 --- a/lib/features/home/views/gallery/search_gallery_view.dart +++ b/lib/features/home/views/gallery/search_gallery_view.dart @@ -29,12 +29,7 @@ class _SearchGalleryView extends StatelessWidget { builder: (context, final searchOption, child) { switch (searchOption) { case SearchOption.local: - return Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, + return CustomWrap( children: [ for (final resultsImageModel in resultsImageModels) CachedNetworkImage( @@ -48,12 +43,7 @@ class _SearchGalleryView extends StatelessWidget { ], ); case SearchOption.web: - return Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, + return CustomWrap( children: [ for (final imageResult in resultsImageModels) Image.network( diff --git a/lib/features/home/views/image_carousel/image_carousel_view.dart b/lib/features/home/views/image_carousel/image_carousel_view.dart index c8f5259..235f2e9 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view.dart @@ -61,13 +61,16 @@ class ImageCarouselView extends StatelessWidget { children: [ ValueListenableBuilder( valueListenable: model.currentImageModelListenable, - builder: (context, _, __) => CachedNetworkImage( - imageUrl: model.currentImageUrl, - cacheKey: model.currentImageKey, - fit: BoxFit.contain, - progressIndicatorBuilder: (_, __, final progress) => - CircularProgressIndicator( - value: model.downloadProgressValue(progress: progress), + builder: (context, _, __) => Hero( + tag: model.currentImageIndex, + child: CachedNetworkImage( + imageUrl: model.currentImageUrl, + cacheKey: model.currentImageKey, + fit: BoxFit.contain, + progressIndicatorBuilder: (_, __, final progress) => + CircularProgressIndicator( + value: model.downloadProgressValue(progress: progress), + ), ), ), ), diff --git a/lib/features/home/views/image_carousel/image_carousel_view_model.dart b/lib/features/home/views/image_carousel/image_carousel_view_model.dart index 695e2df..2a9eb70 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view_model.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view_model.dart @@ -3,7 +3,6 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import '/features/core/abstracts/base_view_model.dart'; import '/features/core/services/logging_service.dart'; -import '/features/core/services/navigation_service.dart'; import '/features/home/services/images_service.dart'; import '/features/home/views/image_carousel/image_carousel_view.dart'; import '/locator.dart'; @@ -12,14 +11,11 @@ import '../../data/models/image_model.dart'; class ImageCarouselViewModel extends BaseViewModel { ImageCarouselViewModel({ required ImagesService imagesService, - required NavigationService navigationService, required LoggingService loggingService, }) : _imagesService = imagesService, - _navigationService = navigationService, _loggingService = loggingService; final ImagesService _imagesService; - final NavigationService _navigationService; final LoggingService _loggingService; late final ValueNotifier _currentImageModelNotifier; diff --git a/lib/features/home/widgets/custom_wrap.dart b/lib/features/home/widgets/custom_wrap.dart new file mode 100644 index 0000000..7f7689c --- /dev/null +++ b/lib/features/home/widgets/custom_wrap.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '/features/core/data/constants/const_colors.dart'; + +class CustomWrap extends StatelessWidget { + const CustomWrap({ + required this.children, + super.key, + }); + + final List children; + + @override + Widget build(BuildContext context) { + return children.isNotEmpty + ? Wrap( + runSpacing: 24, + spacing: 8, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: children, + ) + : const Icon( + Icons.image, + size: 80, + color: ConstColours.red, + ); + } +} diff --git a/lib/l10n/generated/intl/messages_en.dart b/lib/l10n/generated/intl/messages_en.dart index 2b66a59..a9e2d94 100644 --- a/lib/l10n/generated/intl/messages_en.dart +++ b/lib/l10n/generated/intl/messages_en.dart @@ -28,6 +28,9 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "appTitle": MessageLookupByLibrary.simpleMessage("MC Gallery App"), + "errorPageMessage": MessageLookupByLibrary.simpleMessage( + "Oopsie, there has been an error. Hang tight till we do something about it."), "gallery": MessageLookupByLibrary.simpleMessage("Gallery"), "imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"), "imageDetails": MessageLookupByLibrary.simpleMessage( diff --git a/lib/l10n/generated/l10n.dart b/lib/l10n/generated/l10n.dart index d2b457c..360fa09 100644 --- a/lib/l10n/generated/l10n.dart +++ b/lib/l10n/generated/l10n.dart @@ -50,6 +50,16 @@ class Strings { return Localizations.of(context, Strings); } + /// `MC Gallery App` + String get appTitle { + return Intl.message( + 'MC Gallery App', + name: 'appTitle', + desc: '', + args: [], + ); + } + /// `Something went wrong` String get somethingWentWrong { return Intl.message( @@ -60,6 +70,16 @@ class Strings { ); } + /// `Oopsie, there has been an error. Hang tight till we do something about it.` + String get errorPageMessage { + return Intl.message( + 'Oopsie, there has been an error. Hang tight till we do something about it.', + name: 'errorPageMessage', + desc: '', + args: [], + ); + } + /// `Image {imageNumber}: size={imageSide}` String imageNameFetch(Object imageNumber, Object imageSide) { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1880e95..6a2f658 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,8 @@ { "@@locale": "en", + "appTitle": "MC Gallery App", "somethingWentWrong": "Something went wrong", + "errorPageMessage": "Oopsie, there has been an error. Hang tight till we do something about it.", "imageNameFetch": "Image {imageNumber}: size={imageSide}", "imageNameSearch": "Search term '{searchStr}' result: Image {imageNumber}", diff --git a/lib/locator.dart b/lib/locator.dart index 1f10137..64d2c70 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -53,7 +53,6 @@ class Locator { instance().registerFactory( () => ImageCarouselViewModel( imagesService: ImagesService.locate, - navigationService: NavigationService.locate, loggingService: LoggingService.locate, ), );