diff --git a/lib/features/core/data/constants/const_text.dart b/lib/features/core/data/constants/const_text.dart index e69de29..3ac8936 100644 --- a/lib/features/core/data/constants/const_text.dart +++ b/lib/features/core/data/constants/const_text.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +import 'const_colors.dart'; + +abstract class ConstText { + static const _imageOverlayTextStyle = TextStyle( + color: ConstColours.white, + fontSize: 28, + ); + static TextStyle imageOverlayTextStyle(BuildContext context) => _imageOverlayTextStyle; +} diff --git a/lib/features/core/services/overlay_service.dart b/lib/features/core/services/overlay_service.dart index 3ebc266..1514c75 100644 --- a/lib/features/core/services/overlay_service.dart +++ b/lib/features/core/services/overlay_service.dart @@ -11,16 +11,13 @@ class OverlayService { final LoggingService _loggingService; - final Map _animationControllerMap = const {}; final Map _overlayEntryMap = const {}; Future playOverlayEntry({ required BuildContext context, - required AnimationController animationController, required OverlayEntry overlayEntry, }) async { try { - _animationControllerMap[animationController.hashCode] = animationController; _overlayEntryMap[overlayEntry.hashCode] = overlayEntry; Overlay.of( context, @@ -28,12 +25,9 @@ class OverlayService { )! .insert(overlayEntry); - await animationController.forward(); if (overlayEntry.mounted) overlayEntry.remove(); _overlayEntryMap.remove(overlayEntry.hashCode); - animationController.dispose(); - _animationControllerMap.remove(animationController.hashCode); } catch (error, stackTrace) { _loggingService.handle(error, stackTrace); } @@ -46,12 +40,6 @@ class OverlayService { } } _overlayEntryMap.clear(); - for (final animationController in _animationControllerMap.values) { - if (animationController.isAnimating) { - animationController.dispose(); - } - } - _animationControllerMap.clear(); } static OverlayService get locate => Locator.locate(); diff --git a/lib/features/home/api/unsplash_images_api.dart b/lib/features/home/api/unsplash_images_api.dart index 5cde25c..e563367 100644 --- a/lib/features/home/api/unsplash_images_api.dart +++ b/lib/features/home/api/unsplash_images_api.dart @@ -1,29 +1,37 @@ import 'dart:async'; import 'dart:math'; +import 'package:mc_gallery/features/core/services/logging_service.dart'; +import 'package:mc_gallery/locator.dart'; + import '/features/core/data/constants/const_values.dart'; import '/l10n/generated/l10n.dart'; import '../abstracts/images_api.dart'; import '../data/models/image_model.dart'; -class UnsplashImagesApi implements ImagesApi { +class UnsplashImagesApi with LoggingService implements ImagesApi { @override FutureOr> fetchImageUri({required String token}) { final random = Random(); - return Iterable.generate(ConstValues.numberOfImages).map((final imageIndex) { - // Drawing from a normal distribution - final imageSide = ConstValues.minImageSize + - random.nextInt((ConstValues.maxImageSize + 1) - ConstValues.minImageSize); + try { + return Iterable.generate(ConstValues.numberOfImages).map((final imageIndex) { + // Drawing from a normal distribution + final imageSide = ConstValues.minImageSize + + random.nextInt((ConstValues.maxImageSize + 1) - ConstValues.minImageSize); - final imageUri = _imageUrlGenerator(imageSide: imageSide); + final imageUri = _imageUrlGenerator(imageSide: imageSide); - return ImageModel( - imageIndex: imageIndex, - uri: imageUri, - imageName: Strings.current.image, - ); - }); + return ImageModel( + imageIndex: imageIndex, + uri: imageUri, + imageName: '${Strings.current.image} $imageIndex: size=$imageSide', + ); + }); + } on Exception catch (ex, stackTrace) { + handleException(ex, stackTrace); + return const Iterable.empty(); + } } Uri _imageUrlGenerator({required int imageSide}) => Uri( @@ -31,4 +39,6 @@ class UnsplashImagesApi implements ImagesApi { host: ConstValues.backendHost, pathSegments: [...ConstValues.backendUrlPathSegments, '${imageSide}x$imageSide'], ); + + static UnsplashImagesApi get locate => Locator.locate(); } diff --git a/lib/features/home/services/image_cache_manager_service.dart b/lib/features/home/services/image_cache_manager_service.dart new file mode 100644 index 0000000..c0282e4 --- /dev/null +++ b/lib/features/home/services/image_cache_manager_service.dart @@ -0,0 +1,37 @@ +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/logging_service.dart'; +import 'package:mc_gallery/locator.dart'; + +class ImageCacheManagerService with LoggingService { + ImageCacheManagerService({ + required AppLifecycleService appLifecycleService, + }) : _appLifecycleService = appLifecycleService { + _init(); + } + + final AppLifecycleService _appLifecycleService; + + Future emptyCache() async => await DefaultCacheManager().emptyCache(); + + Future _init() async { + _appLifecycleService.addListener( + tag: runtimeType.toString(), + listener: (final appLifecycleState) async { + switch (appLifecycleState) { + case AppLifecycleState.resumed: + break; + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + info('Discarding cached images'); + await DefaultCacheManager().emptyCache(); + } + }, + ); + } + + static ImageCacheManagerService get locate => Locator.locate(); +} diff --git a/lib/features/home/services/images_service.dart b/lib/features/home/services/images_service.dart index 0086262..d26e5c1 100644 --- a/lib/features/home/services/images_service.dart +++ b/lib/features/home/services/images_service.dart @@ -1,3 +1,4 @@ +import 'package:mc_gallery/features/core/services/logging_service.dart'; import 'package:mc_gallery/features/home/data/models/image_model.dart'; import 'package:mc_gallery/locator.dart'; @@ -8,20 +9,33 @@ import '../abstracts/images_api.dart'; /// Since this is very simple use-case, this is the only interface. For complex (actual CRUD-based) I/O, /// an additional Repository layer interface can be used between [ImagesService] and [ImagesApi]. class ImagesService { - ImagesService({required ImagesApi imagesApi}) : _imagesApi = imagesApi { + ImagesService({ + required ImagesApi imagesApi, + required LoggingService loggingService, + }) : _imagesApi = imagesApi, + _loggingService = loggingService { _init(); } final ImagesApi _imagesApi; + final LoggingService _loggingService; late final Iterable _imageModels; Iterable get imageModels => _imageModels; Future _init() async { + _loggingService.info('Fetching and creating image models...'); _imageModels = await _imagesApi.fetchImageUri(token: ''); + _imageModels.isNotEmpty + ? _loggingService.good("Created ${_imageModels.length} images' models") + : _loggingService.warning('No images found'); + Locator.instance().signalReady(this); } + int get firstAvailableImageIndex => 0; + int get lastAvailableImageIndex => _imageModels.length - 1; + static ImagesService get locate => Locator.locate(); } diff --git a/lib/features/home/views/gallery/gallery_view_model.dart b/lib/features/home/views/gallery/gallery_view_model.dart index fcacf41..5128a2f 100644 --- a/lib/features/home/views/gallery/gallery_view_model.dart +++ b/lib/features/home/views/gallery/gallery_view_model.dart @@ -1,12 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:mc_gallery/features/core/services/app_lifecycle_service.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/data/models/image_model.dart'; +import '/features/home/services/image_cache_manager_service.dart'; import '/features/home/services/images_service.dart'; import '/features/home/views/image_carousel/image_carousel_view.dart'; import '/locator.dart'; @@ -15,16 +15,16 @@ class GalleryViewModel extends BaseViewModel { GalleryViewModel({ required ImagesService imagesService, required NavigationService navigationService, - required AppLifecycleService appLifecycleService, + required ImageCacheManagerService imageCacheManagerService, required LoggingService loggingService, }) : _imagesService = imagesService, _navigationService = navigationService, - _appLifecycleService = appLifecycleService, + _imageCacheManagerService = imageCacheManagerService, _loggingService = loggingService; final ImagesService _imagesService; final NavigationService _navigationService; - final AppLifecycleService _appLifecycleService; + final ImageCacheManagerService _imageCacheManagerService; final LoggingService _loggingService; final ValueNotifier _isDisplayingPressingPrompt = ValueNotifier(true); @@ -32,27 +32,11 @@ class GalleryViewModel extends BaseViewModel { @override Future initialise(bool Function() mounted, [arguments]) async { - _appLifecycleService.addListener( - tag: runtimeType.toString(), - listener: (final appLifecycleState) async { - switch (appLifecycleState) { - case AppLifecycleState.resumed: - break; - case AppLifecycleState.inactive: - case AppLifecycleState.paused: - case AppLifecycleState.detached: - await DefaultCacheManager().emptyCache(); - } - }, - ); - super.initialise(mounted, arguments); } @override Future dispose() async { - await _appLifecycleService.removeListener(tag: runtimeType.toString()); - super.dispose(); } 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 2a37c71..13dae54 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view.dart @@ -1,6 +1,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:mc_gallery/features/core/data/constants/const_colors.dart'; +import 'package:mc_gallery/features/core/data/constants/const_text.dart'; +import '/features/core/widgets/gap.dart'; import '/features/core/widgets/mcg_scaffold.dart'; import '/features/core/widgets/view_model_builder.dart'; import '/features/home/views/image_carousel/image_carousel_view_model.dart'; @@ -27,15 +31,56 @@ class ImageCarouselView extends StatelessWidget { appBar: AppBar( title: Text(model.strings.imageCarousel), ), - body: Hero( - tag: model.currentImageKey, - child: CachedNetworkImage( - imageUrl: model.currentImageUrl, - cacheKey: model.currentImageKey, - progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator( - value: model.downloadProgressValue(progress: progress), + body: Column( + children: [ + Expanded( + child: Card( + elevation: 8, + child: Stack( + fit: StackFit.expand, + children: [ + Hero( + tag: model.currentImageKey, + child: CachedNetworkImage( + imageUrl: model.currentImageUrl, + cacheKey: model.currentImageKey, + fit: BoxFit.fill, + progressIndicatorBuilder: (_, __, final progress) => + CircularProgressIndicator( + value: model.downloadProgressValue(progress: progress), + ), + ), + ), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + Icons.chevron_left, + color: model.hasPreviousImage ? ConstColours.white : ConstColours.black, + ), + Text( + model.currentImageName, + style: ConstText.imageOverlayTextStyle(context), + ), + Icon( + Icons.chevron_right, + color: model.hasNextImage ? ConstColours.white : ConstColours.black, + ), + ], + ), + ), + ], + ), + ), ), - ), + const Gap(24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: MarkdownBody(data: model.strings.imageDetails), + ), + const Gap(16), + ], ), ), ); 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 76dc40f..a9579bc 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 @@ -1,8 +1,5 @@ -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart'; import '/features/core/abstracts/base_view_model.dart'; import '/features/core/services/logging_service.dart'; @@ -16,16 +13,13 @@ class ImageCarouselViewModel extends BaseViewModel { ImageCarouselViewModel({ required ImagesService imagesService, required NavigationService navigationService, - required AppLifecycleService appLifecycleService, required LoggingService loggingService, }) : _imagesService = imagesService, _navigationService = navigationService, - _appLifecycleService = appLifecycleService, _loggingService = loggingService; final ImagesService _imagesService; final NavigationService _navigationService; - final AppLifecycleService _appLifecycleService; final LoggingService _loggingService; late final ValueNotifier _currentImageModel; @@ -33,19 +27,6 @@ class ImageCarouselViewModel extends BaseViewModel { @override Future initialise(bool Function() mounted, [arguments]) async { - _appLifecycleService.addListener( - tag: runtimeType.toString(), - listener: (final appLifecycleState) async { - switch (appLifecycleState) { - case AppLifecycleState.resumed: - break; - case AppLifecycleState.inactive: - case AppLifecycleState.paused: - case AppLifecycleState.detached: - await DefaultCacheManager().emptyCache(); - } - }); - _currentImageModel = ValueNotifier(_imagesService.imageModels .elementAt((arguments! as ImageCarouselViewArguments).imageIndexKey)); @@ -54,15 +35,20 @@ class ImageCarouselViewModel extends BaseViewModel { @override Future dispose() async { - await _appLifecycleService.removeListener(tag: runtimeType.toString()); - super.dispose(); } String get currentImageUrl => currentImageModel.value.uri.toString(); String get currentImageKey => currentImageModel.value.imageIndex.toString(); + String get currentImageName => currentImageModel.value.imageName; + double? downloadProgressValue({required DownloadProgress progress}) => progress.totalSize != null ? progress.downloaded / progress.totalSize! : null; + bool get hasPreviousImage => + currentImageModel.value.imageIndex > _imagesService.firstAvailableImageIndex; + bool get hasNextImage => + currentImageModel.value.imageIndex < _imagesService.lastAvailableImageIndex; + static ImageCarouselViewModel get locate => Locator.locate(); } diff --git a/lib/l10n/generated/intl/messages_en.dart b/lib/l10n/generated/intl/messages_en.dart index 65ff4af..ed85749 100644 --- a/lib/l10n/generated/intl/messages_en.dart +++ b/lib/l10n/generated/intl/messages_en.dart @@ -25,6 +25,8 @@ class MessageLookup extends MessageLookupByLibrary { "gallery": MessageLookupByLibrary.simpleMessage("Gallery"), "image": MessageLookupByLibrary.simpleMessage("Image"), "imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"), + "imageDetails": MessageLookupByLibrary.simpleMessage( + "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?"), "somethingWentWrong": MessageLookupByLibrary.simpleMessage("Something went wrong"), "startLoadingPrompt": diff --git a/lib/l10n/generated/l10n.dart b/lib/l10n/generated/l10n.dart index 773df27..7c4f39c 100644 --- a/lib/l10n/generated/l10n.dart +++ b/lib/l10n/generated/l10n.dart @@ -99,6 +99,16 @@ class Strings { args: [], ); } + + /// `Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?` + String get imageDetails { + return Intl.message( + 'Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?', + name: 'imageDetails', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0ba0c6d..e08a39f 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -7,5 +7,6 @@ "gallery": "Gallery", "startLoadingPrompt": "Press me to start loading", - "imageCarousel": "Image carousel" + "imageCarousel": "Image carousel", + "imageDetails": "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?" } \ No newline at end of file diff --git a/lib/locator.dart b/lib/locator.dart index 61b4482..cb1b0f1 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -7,6 +7,7 @@ import 'package:mc_gallery/features/core/abstracts/router/app_router.dart'; import 'package:mc_gallery/features/core/services/logging_service.dart'; import 'package:mc_gallery/features/core/services/navigation_service.dart'; import 'package:mc_gallery/features/home/api/unsplash_images_api.dart'; +import 'package:mc_gallery/features/home/services/image_cache_manager_service.dart'; import 'package:mc_gallery/features/home/services/images_service.dart'; import 'package:mc_gallery/features/home/views/gallery/gallery_view_model.dart'; import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view_model.dart'; @@ -44,7 +45,7 @@ class Locator { () => GalleryViewModel( imagesService: ImagesService.locate, navigationService: NavigationService.locate, - appLifecycleService: AppLifecycleService.locate, + imageCacheManagerService: ImageCacheManagerService.locate, loggingService: LoggingService.locate, ), ); @@ -52,7 +53,6 @@ class Locator { () => ImageCarouselViewModel( imagesService: ImagesService.locate, navigationService: NavigationService.locate, - appLifecycleService: AppLifecycleService.locate, loggingService: LoggingService.locate, ), ); @@ -86,11 +86,14 @@ class Locator { dispose: (param) async => await param.dispose(), ); it.registerSingleton( - ImagesService( - imagesApi: UnsplashImagesApi(), - ), + ImagesService(imagesApi: UnsplashImagesApi.locate, loggingService: LoggingService.locate), signalsReady: true, ); + it.registerSingleton( + ImageCacheManagerService( + appLifecycleService: AppLifecycleService.locate, + ), + ); } static FutureOr _registerRepos(GetIt locator) {} diff --git a/pubspec.lock b/pubspec.lock index a7df332..9cbd01a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -195,6 +195,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.13" flutter_svg: dependency: "direct main" description: @@ -289,6 +296,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2b08fd..637ddb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ dependencies: connectivity_plus: ^3.0.2 internet_connection_checker: ^1.0.0+1 + # Util frontend + flutter_markdown: ^0.6.13 + # Logging talker: ^2.1.0+1 talker_dio_logger: ^1.0.0