diff --git a/lib/features/core/data/constants/const_values.dart b/lib/features/core/data/constants/const_values.dart index 903ab9a..a468f1b 100644 --- a/lib/features/core/data/constants/const_values.dart +++ b/lib/features/core/data/constants/const_values.dart @@ -6,4 +6,6 @@ abstract class ConstValues { static const int numberOfImages = 20; static const int minImageSize = 50; static const int maxImageSize = 100; + + static const int defaultEmulatedLatencyMillis = 75; } diff --git a/lib/features/core/data/extensions/iterable_extensions.dart b/lib/features/core/data/extensions/iterable_extensions.dart new file mode 100644 index 0000000..69845c8 --- /dev/null +++ b/lib/features/core/data/extensions/iterable_extensions.dart @@ -0,0 +1,3 @@ +extension IterableExtensions on Iterable { + Iterable get deepCopy => toList(growable: false); +} diff --git a/lib/features/core/data/extensions/random_extensions.dart b/lib/features/core/data/extensions/random_extensions.dart new file mode 100644 index 0000000..117b55e --- /dev/null +++ b/lib/features/core/data/extensions/random_extensions.dart @@ -0,0 +1,9 @@ +import 'dart:math'; + +extension RandomExtensions on Random { + int nextIntInRange({ + required int min, + required int max, + }) => + min + nextInt(max + 1 - min); +} diff --git a/lib/features/core/utils/mutex.dart b/lib/features/core/utils/mutex.dart new file mode 100644 index 0000000..aa8ecce --- /dev/null +++ b/lib/features/core/utils/mutex.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:ui'; + +/// A simple Mutex implementation using a [Completer]. +class Mutex { + final _completerQueue = Queue(); + + /// Runs the given [run] function-block in a thread-safe/blocked zone. A convenient `unlock()` + /// is provided, which can be called anywhere to signal re-entry. + FutureOr lockAndRun({ + required FutureOr Function(VoidCallback unlock) run, + }) async { + final completer = Completer(); + _completerQueue.add(completer); + if (_completerQueue.first != completer) { + await _completerQueue.removeFirst().future; + } + final value = await run(() => completer.complete()); + return value; + } + + Future get lastOperationCompletionAwaiter => + _completerQueue.isNotEmpty ? _completerQueue.last.future : Future.value(); +} diff --git a/lib/features/core/widgets/state/multi_value_listenable_builder.dart b/lib/features/core/widgets/state/multi_value_listenable_builder.dart new file mode 100644 index 0000000..fb8be33 --- /dev/null +++ b/lib/features/core/widgets/state/multi_value_listenable_builder.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart' show Listenable, ValueListenable; +import 'package:flutter/widgets.dart'; + +/// This widget listens to multiple [ValueListenable]s and calls given builder function if any one of them changes. +class MultiValueListenableBuilder extends StatelessWidget { + const MultiValueListenableBuilder({ + required this.valueListenables, + required this.builder, + this.child, + super.key, + }) : assert(valueListenables.length != 0); + + /// List of [ValueListenable]s to be listened to. + final List valueListenables; + + /// The builder function to be called when value of any of the [ValueListenable] changes. + /// The order of values list will be same as [valueListenables] list. + final Widget Function(BuildContext context, List values, Widget? child) builder; + + /// An optional child widget which will be available as child parameter in [builder]. + final Widget? child; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: Listenable.merge(valueListenables), + builder: (context, child) { + final providedValues = valueListenables.map((final listenable) => listenable.value); + return builder(context, List.unmodifiable(providedValues), child); + }, + child: child, + ); + } +} diff --git a/lib/features/core/widgets/view_model_builder.dart b/lib/features/core/widgets/state/view_model_builder.dart similarity index 96% rename from lib/features/core/widgets/view_model_builder.dart rename to lib/features/core/widgets/state/view_model_builder.dart index 66de637..f503dd0 100644 --- a/lib/features/core/widgets/view_model_builder.dart +++ b/lib/features/core/widgets/state/view_model_builder.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import '../abstracts/base_view_model.dart'; +import '../../abstracts/base_view_model.dart'; class ViewModelBuilder extends StatefulWidget { const ViewModelBuilder({ diff --git a/lib/features/home/abstracts/images_api.dart b/lib/features/home/abstracts/images_api.dart index 598f22a..a0b2475 100644 --- a/lib/features/home/abstracts/images_api.dart +++ b/lib/features/home/abstracts/images_api.dart @@ -8,4 +8,9 @@ import '../data/models/image_model.dart'; /// and convoluting) interface is for adding a bit of flexibility to change strategy to some other site. abstract class ImagesApi { FutureOr> fetchImageUri({required String token}); + + FutureOr> searchImages({ + required String searchStr, + required String token, + }); } diff --git a/lib/features/home/api/unsplash_images_api.dart b/lib/features/home/api/unsplash_images_api.dart index 82b8f84..8efe727 100644 --- a/lib/features/home/api/unsplash_images_api.dart +++ b/lib/features/home/api/unsplash_images_api.dart @@ -1,39 +1,80 @@ 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 '/features/core/data/extensions/random_extensions.dart'; +import '/features/core/services/logging_service.dart'; import '/l10n/generated/l10n.dart'; +import '/locator.dart'; import '../abstracts/images_api.dart'; import '../data/models/image_model.dart'; -class UnsplashImagesApi with LoggingService implements ImagesApi { +class UnsplashImagesApi implements ImagesApi { + final LoggingService _loggingService = LoggingService.locate; + final random = Random(); + @override - FutureOr> fetchImageUri({required String token}) { - final random = Random(); + FutureOr> fetchImageUri({required String token}) async { + // Dummy fetching delay emulation + await Future.delayed(const Duration( + milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages)); try { + // Create fixed number of images 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 imageSide = + random.nextIntInRange(min: ConstValues.minImageSize, max: ConstValues.maxImageSize); final imageUri = _imageUrlGenerator(imageSide: imageSide); return ImageModel( imageIndex: imageIndex, uri: imageUri, - imageName: '${Strings.current.image} ${imageIndex + 1}: size=$imageSide', + // Custom dummy name for the image + + imageName: Strings.current.imageNameFetch(imageIndex + 1, imageSide), ); }); } on Exception catch (ex, stackTrace) { - handleException(ex, stackTrace); + _loggingService.handleException(ex, stackTrace); return const Iterable.empty(); } } + @override + FutureOr> searchImages({ + required String searchStr, + required String token, + }) async { + final numberOfResults = random.nextIntInRange(min: 0, max: ConstValues.numberOfImages); + + // Dummy fetching delay emulation + await Future.delayed( + Duration(milliseconds: ConstValues.defaultEmulatedLatencyMillis * numberOfResults)); + + try { + // Create (randomly-bounded) dummy number of images + return Iterable.generate(numberOfResults).map((final imageIndex) { + // Drawing from a normal distribution + final imageSide = + random.nextIntInRange(min: ConstValues.minImageSize, max: ConstValues.maxImageSize); + + final imageUri = _imageUrlGenerator(imageSide: imageSide); + + return ImageModel( + imageIndex: imageIndex, + uri: imageUri, + // Custom dummy name for the image + imageName: Strings.current.imageNameSearch(searchStr, imageIndex + 1), + ); + }).toList(growable: false); + } on Exception catch (ex, stackTrace) { + _loggingService.handleException(ex, stackTrace); + return List.empty(); + } + } + Uri _imageUrlGenerator({required int imageSide}) => Uri( scheme: ConstValues.httpsScheme, host: ConstValues.backendHost, diff --git a/lib/features/home/data/enums/search_option.dart b/lib/features/home/data/enums/search_option.dart new file mode 100644 index 0000000..f95cdf9 --- /dev/null +++ b/lib/features/home/data/enums/search_option.dart @@ -0,0 +1,15 @@ +import '/l10n/generated/l10n.dart'; + +enum SearchOption { + local, + web; + + String get name { + switch (this) { + case SearchOption.local: + return Strings.current.local; + case SearchOption.web: + return Strings.current.web; + } + } +} diff --git a/lib/features/home/services/images_service.dart b/lib/features/home/services/images_service.dart index ce04a3a..0bf756c 100644 --- a/lib/features/home/services/images_service.dart +++ b/lib/features/home/services/images_service.dart @@ -1,8 +1,12 @@ -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'; +import 'dart:async'; +import '/features/core/data/extensions/iterable_extensions.dart'; +import '/features/core/services/logging_service.dart'; +import '/features/core/utils/mutex.dart'; +import '/features/home/data/models/image_model.dart'; +import '/locator.dart'; import '../abstracts/images_api.dart'; +import '../data/enums/search_option.dart'; /// Handles fetching and storing of Images. /// @@ -21,7 +25,14 @@ class ImagesService { final LoggingService _loggingService; late final Iterable _imageModels; - Iterable get imageModels => _imageModels; + Iterable get imageModels => _imageModels.deepCopy; + final Mutex _searchMutex = Mutex(); + + /// Manual initialization triggering + /// + /// Since this service not critical to app-start-up [Locator]'s `signalsReady=true` is not used. + final Completer _initAwaiter = Completer(); + Future get initAwaiter => _initAwaiter.future; Future _init() async { _loggingService.info('Fetching and creating image models...'); @@ -31,7 +42,7 @@ class ImagesService { ? _loggingService.good("Created ${_imageModels.length} images' models") : _loggingService.warning('No images found'); - Locator.instance().signalReady(this); + _initAwaiter.complete(); } int get firstAvailableImageIndex => 0; @@ -40,5 +51,28 @@ class ImagesService { ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index); + Future get lastQueryIsCompleted => _searchMutex.lastOperationCompletionAwaiter; + + Future> searchImages({ + required SearchOption searchOption, + required String imageNamePart, + }) async { + return await _searchMutex.lockAndRun(run: (final unlock) async { + try { + switch (searchOption) { + case SearchOption.local: + return []; + case SearchOption.web: + return await _imagesApi.searchImages( + searchStr: imageNamePart, + token: '', + ); + } + } finally { + unlock(); + } + }); + } + static ImagesService get locate => Locator.locate(); } diff --git a/lib/features/home/views/gallery/downloaded_gallery_view.dart b/lib/features/home/views/gallery/downloaded_gallery_view.dart new file mode 100644 index 0000000..36b9746 --- /dev/null +++ b/lib/features/home/views/gallery/downloaded_gallery_view.dart @@ -0,0 +1,44 @@ +part of 'gallery_view.dart'; + +class _DownloadedGalleryView extends StatelessWidget { + const _DownloadedGalleryView({ + required this.galleryViewModel, + super.key, + }); + + final GalleryViewModel galleryViewModel; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour), + child: Padding( + padding: const EdgeInsets.all(8), + // Using Wrap instead of GridView, to make use of different image sizes + child: Wrap( + runSpacing: 24, + spacing: 8, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (final imageModel in galleryViewModel.imageModels) + GestureDetector( + onTap: () => galleryViewModel.pushImageCarouselView( + context, + imageModel: imageModel, + ), + child: CachedNetworkImage( + imageUrl: imageModel.uri.toString(), + cacheKey: imageModel.imageIndex.toString(), + progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator( + value: 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 b517f53..75a01c4 100644 --- a/lib/features/home/views/gallery/gallery_view.dart +++ b/lib/features/home/views/gallery/gallery_view.dart @@ -3,10 +3,17 @@ import 'package:flutter/material.dart'; import '/features/core/data/constants/const_colors.dart'; import '/features/core/data/constants/const_durations.dart'; +import '/features/core/widgets/gap.dart'; import '/features/core/widgets/mcg_scaffold.dart'; -import '/features/core/widgets/view_model_builder.dart'; +import '/features/core/widgets/state/multi_value_listenable_builder.dart'; +import '/features/core/widgets/state/view_model_builder.dart'; +import '../../data/enums/search_option.dart'; +import '../../data/models/image_model.dart'; import 'gallery_view_model.dart'; +part 'downloaded_gallery_view.dart'; +part 'search_gallery_view.dart'; + class GalleryView extends StatelessWidget { const GalleryView({super.key}); @@ -18,7 +25,36 @@ class GalleryView extends StatelessWidget { bodyBuilderWaiter: model.isInitialised, forceInternetCheck: true, appBar: AppBar( - title: Text(model.strings.gallery), + centerTitle: true, + primary: true, + title: ValueListenableBuilder( + valueListenable: model.isSearchingListenable, + builder: (context, final isSearching, _) => AnimatedSwitcher( + duration: ConstDurations.quarterDefaultAnimationDuration, + child: !isSearching + ? Center(child: Text(model.strings.gallery)) + : _SearchBox(galleryViewModel: model), + ), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 40), + child: MultiValueListenableBuilder( + valueListenables: [ + model.isDisplayingPressingPrompt, + model.isSearchingListenable, + ], + builder: (context, final values, child) => !model.isDisplayingPressingPrompt.value + ? IconButton( + icon: !model.isSearchingListenable.value + ? const Icon(Icons.search) + : const Icon(Icons.close), + onPressed: model.searchPressed, + ) + : const SizedBox.shrink(), + ), + ) + ], ), body: Center( child: ValueListenableBuilder( @@ -30,34 +66,27 @@ class GalleryView extends StatelessWidget { onPressed: model.onPromptPressed, child: Text(model.strings.startLoadingPrompt), ) - : DecoratedBox( - decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour), - child: SingleChildScrollView( - // Using Wrap instead of GridView, to make use of different image sizes - child: Wrap( - runSpacing: 24, - spacing: 8, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - for (final imageModel in model.imageModels) - GestureDetector( - onTap: () => model.pushImageCarouselView( - context, - imageModel: imageModel, + : SingleChildScrollView( + child: FutureBuilder( + future: model.initImageFetchIsDone, + builder: (context, final snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return const CircularProgressIndicator(); + case ConnectionState.done: + return ValueListenableBuilder( + valueListenable: model.isSearchingListenable, + builder: (context, final isSearching, _) => AnimatedSwitcher( + duration: ConstDurations.oneAndHalfDefaultAnimationDuration, + child: !isSearching + ? _DownloadedGalleryView(galleryViewModel: model) + : _SearchGalleryView(galleryViewModel: model), ), - child: CachedNetworkImage( - imageUrl: imageModel.uri.toString(), - cacheKey: imageModel.imageIndex.toString(), - progressIndicatorBuilder: (_, __, final progress) => - CircularProgressIndicator( - value: model.downloadProgressValue(progress: progress), - ), - ), - ), - ], - ), + ); + } + }, ), ), ), @@ -67,3 +96,49 @@ class GalleryView extends StatelessWidget { ); } } + +class _SearchBox extends StatelessWidget { + const _SearchBox({ + required this.galleryViewModel, + super.key, + }); + + final GalleryViewModel galleryViewModel; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + ValueListenableBuilder( + valueListenable: galleryViewModel.searchOptionListenable, + builder: (context, final searchOption, _) => Padding( + padding: const EdgeInsets.only(top: 8), + child: DropdownButton( + underline: const SizedBox.shrink(), + borderRadius: BorderRadius.circular(24), + items: [ + for (final searchOption in SearchOption.values) + DropdownMenuItem( + child: Center(child: Text(searchOption.name)), + value: searchOption, + ), + ], + value: searchOption, + onChanged: galleryViewModel.onSearchOptionChanged, + ), + ), + ), + const Gap(18), + Expanded( + child: TextField( + autofocus: true, + decoration: InputDecoration( + hintText: galleryViewModel.strings.searchForImage, + ), + onChanged: galleryViewModel.onSearchTermUpdate, + ), + ), + ], + ); + } +} diff --git a/lib/features/home/views/gallery/gallery_view_model.dart b/lib/features/home/views/gallery/gallery_view_model.dart index 4e213d8..f6e274b 100644 --- a/lib/features/home/views/gallery/gallery_view_model.dart +++ b/lib/features/home/views/gallery/gallery_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; @@ -10,6 +12,7 @@ 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'; +import '../../data/enums/search_option.dart'; class GalleryViewModel extends BaseViewModel { GalleryViewModel({ @@ -30,6 +33,13 @@ class GalleryViewModel extends BaseViewModel { final ValueNotifier _isDisplayingPressingPrompt = ValueNotifier(true); ValueListenable get isDisplayingPressingPrompt => _isDisplayingPressingPrompt; + final ValueNotifier _isSearchingNotifier = ValueNotifier(false); + ValueListenable get isSearchingListenable => _isSearchingNotifier; + final ValueNotifier _searchOptionNotifier = ValueNotifier(SearchOption.web); + ValueListenable get searchOptionListenable => _searchOptionNotifier; + final ValueNotifier> _imageSearchResultsNotifier = ValueNotifier([]); + ValueListenable> get imageSearchResultsListenable => _imageSearchResultsNotifier; + @override Future initialise(bool Function() mounted, [arguments]) async { super.initialise(mounted, arguments); @@ -40,9 +50,44 @@ class GalleryViewModel extends BaseViewModel { super.dispose(); } + Future onSearchTermUpdate(String searchTerm) async { + // If empty-string (from backspacing) -> reset state. + if (searchTerm.isEmpty) { + _imageSearchResultsNotifier.value = []; + return; + } + + // Detached call to prevent UI blocking + unawaited(_imagesService + .searchImages( + imageNamePart: searchTerm, + searchOption: searchOptionListenable.value, + ) + .then( + (final fetchedImageModels) => _imageSearchResultsNotifier.value = fetchedImageModels)); + + // Force-update to trigger listening to `lastQueryResultDone()`. + _imageSearchResultsNotifier.notifyListeners(); + } + + void searchPressed() { + // If transitioning from 'Searching', clear previous results immediately + if (_isSearchingNotifier.value) _imageSearchResultsNotifier.value = []; + + _isSearchingNotifier.value = !_isSearchingNotifier.value; + } + + Future get lastQueryResultDone => _imagesService.lastQueryIsCompleted; + + void onSearchOptionChanged(SearchOption? option) => _searchOptionNotifier.value = option!; + void onPromptPressed() => _isDisplayingPressingPrompt.value = false; Iterable get imageModels => _imagesService.imageModels; + Future get initImageFetchIsDone => _imagesService.initAwaiter; + + double? downloadProgressValue({required DownloadProgress progress}) => + progress.totalSize != null ? progress.downloaded / progress.totalSize! : null; void pushImageCarouselView(BuildContext context, {required ImageModel imageModel}) => _navigationService.pushImageCarouselView( @@ -53,7 +98,4 @@ class GalleryViewModel extends BaseViewModel { ); static GalleryViewModel get locate => Locator.locate(); - - double? downloadProgressValue({required DownloadProgress progress}) => - progress.totalSize != null ? progress.downloaded / progress.totalSize! : null; } diff --git a/lib/features/home/views/gallery/search_gallery_view.dart b/lib/features/home/views/gallery/search_gallery_view.dart new file mode 100644 index 0000000..19d9e34 --- /dev/null +++ b/lib/features/home/views/gallery/search_gallery_view.dart @@ -0,0 +1,61 @@ +part of 'gallery_view.dart'; + +class _SearchGalleryView extends StatelessWidget { + const _SearchGalleryView({ + required this.galleryViewModel, + super.key, + }); + + final GalleryViewModel galleryViewModel; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: galleryViewModel.imageSearchResultsListenable, + builder: (context, final resultsImageModels, _) => FutureBuilder( + future: galleryViewModel.lastQueryResultDone, + builder: (context, final snapshot) { + final Widget displayedWidget; + + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + displayedWidget = const CircularProgressIndicator(); + break; + case ConnectionState.done: + displayedWidget = Wrap( + runSpacing: 24, + spacing: 8, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (final imageResult in resultsImageModels) + Image.network( + imageResult.uri.toString(), + loadingBuilder: (context, final child, final loadingProgress) => + loadingProgress == null + ? child + : Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ), + ], + ); + } + + return AnimatedSwitcher( + duration: ConstDurations.halfDefaultAnimationDuration, + child: displayedWidget, + ); + }, + ), + ); + } +} 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 af30987..f0a34fb 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view.dart @@ -9,8 +9,8 @@ import '/features/core/data/constants/const_colors.dart'; import '/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'; +import '../../../core/widgets/state/view_model_builder.dart'; class ImageCarouselViewArguments { const ImageCarouselViewArguments({required this.imageIndexKey}); diff --git a/lib/l10n/generated/intl/messages_en.dart b/lib/l10n/generated/intl/messages_en.dart index 46b3ea6..2b66a59 100644 --- a/lib/l10n/generated/intl/messages_en.dart +++ b/lib/l10n/generated/intl/messages_en.dart @@ -20,18 +20,29 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; + static String m0(imageNumber, imageSide) => + "Image ${imageNumber}: size=${imageSide}"; + + static String m1(searchStr, imageNumber) => + "Search term \'${searchStr}\' result: Image ${imageNumber}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "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?"), + "imageNameFetch": m0, + "imageNameSearch": m1, + "local": MessageLookupByLibrary.simpleMessage("Local"), "noInternetMessage": MessageLookupByLibrary.simpleMessage( "Are you sure that you\'re connected to the internet?"), + "searchForImage": + MessageLookupByLibrary.simpleMessage("Search for your image"), "somethingWentWrong": MessageLookupByLibrary.simpleMessage("Something went wrong"), "startLoadingPrompt": - MessageLookupByLibrary.simpleMessage("Press me to start loading") + MessageLookupByLibrary.simpleMessage("Press me to start loading"), + "web": MessageLookupByLibrary.simpleMessage("Web") }; } diff --git a/lib/l10n/generated/l10n.dart b/lib/l10n/generated/l10n.dart index ec0ad09..d2b457c 100644 --- a/lib/l10n/generated/l10n.dart +++ b/lib/l10n/generated/l10n.dart @@ -60,13 +60,23 @@ class Strings { ); } - /// `Image` - String get image { + /// `Image {imageNumber}: size={imageSide}` + String imageNameFetch(Object imageNumber, Object imageSide) { return Intl.message( - 'Image', - name: 'image', + 'Image $imageNumber: size=$imageSide', + name: 'imageNameFetch', desc: '', - args: [], + args: [imageNumber, imageSide], + ); + } + + /// `Search term '{searchStr}' result: Image {imageNumber}` + String imageNameSearch(Object searchStr, Object imageNumber) { + return Intl.message( + 'Search term \'$searchStr\' result: Image $imageNumber', + name: 'imageNameSearch', + desc: '', + args: [searchStr, imageNumber], ); } @@ -90,6 +100,36 @@ class Strings { ); } + /// `Search for your image` + String get searchForImage { + return Intl.message( + 'Search for your image', + name: 'searchForImage', + desc: '', + args: [], + ); + } + + /// `Local` + String get local { + return Intl.message( + 'Local', + name: 'local', + desc: '', + args: [], + ); + } + + /// `Web` + String get web { + return Intl.message( + 'Web', + name: 'web', + desc: '', + args: [], + ); + } + /// `Image carousel` String get imageCarousel { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9f04702..1880e95 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -2,10 +2,14 @@ "@@locale": "en", "somethingWentWrong": "Something went wrong", - "image": "Image", + "imageNameFetch": "Image {imageNumber}: size={imageSide}", + "imageNameSearch": "Search term '{searchStr}' result: Image {imageNumber}", "gallery": "Gallery", "startLoadingPrompt": "Press me to start loading", + "searchForImage": "Search for your image", + "local": "Local", + "web": "Web", "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?", diff --git a/lib/locator.dart b/lib/locator.dart index a0f6706..ecc5b57 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -96,9 +96,8 @@ class Locator { it.registerSingleton( ImagesService(imagesApi: UnsplashImagesApi.locate, loggingService: LoggingService.locate), - signalsReady: true, ); - await it.isReady(); + //await it.isReady(); it.registerSingleton( ImageCacheManagerService(