live search

This commit is contained in:
Mehul Ahal 2022-12-23 11:20:46 +01:00
parent 730db86b80
commit aed02b302b
17 changed files with 471 additions and 51 deletions

View File

@ -0,0 +1,3 @@
extension IterableExtensions<T> on Iterable<T> {
Iterable<T> get deepCopy => toList(growable: false);
}

View File

@ -0,0 +1,9 @@
import 'dart:math';
extension RandomExtensions on Random {
int nextIntInRange({
required int min,
required int max,
}) =>
min + nextInt(max + 1 - min);
}

View File

@ -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<Completer>();
/// 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<T> lockAndRun<T>({
required FutureOr<T> 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<void> get lastOperationCompletionAwaiter =>
_completerQueue.isNotEmpty ? _completerQueue.last.future : Future.value();
}

View File

@ -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<ValueListenable> 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<dynamic> 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<dynamic>.unmodifiable(providedValues), child);
},
child: child,
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../abstracts/base_view_model.dart'; import '../../abstracts/base_view_model.dart';
class ViewModelBuilder<T extends BaseViewModel> extends StatefulWidget { class ViewModelBuilder<T extends BaseViewModel> extends StatefulWidget {
const ViewModelBuilder({ const ViewModelBuilder({

View File

@ -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. /// and convoluting) interface is for adding a bit of flexibility to change strategy to some other site.
abstract class ImagesApi { abstract class ImagesApi {
FutureOr<Iterable<ImageModel>> fetchImageUri({required String token}); FutureOr<Iterable<ImageModel>> fetchImageUri({required String token});
FutureOr<List<ImageModel>> searchImages({
required String searchStr,
required String token,
});
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:mc_gallery/features/core/data/extensions/random_extensions.dart';
import 'package:mc_gallery/features/core/services/logging_service.dart'; import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/locator.dart'; import 'package:mc_gallery/locator.dart';
@ -9,31 +10,65 @@ import '/l10n/generated/l10n.dart';
import '../abstracts/images_api.dart'; import '../abstracts/images_api.dart';
import '../data/models/image_model.dart'; import '../data/models/image_model.dart';
class UnsplashImagesApi with LoggingService implements ImagesApi { class UnsplashImagesApi implements ImagesApi {
@override final LoggingService _loggingService = LoggingService.locate;
FutureOr<Iterable<ImageModel>> fetchImageUri({required String token}) { final random = Random();
final random = Random();
@override
FutureOr<Iterable<ImageModel>> fetchImageUri({required String token}) async {
try { try {
// Create fixed number of images
return Iterable<int>.generate(ConstValues.numberOfImages).map((final imageIndex) { return Iterable<int>.generate(ConstValues.numberOfImages).map((final imageIndex) {
// Drawing from a normal distribution // Drawing from a normal distribution
final imageSide = ConstValues.minImageSize + final imageSide =
random.nextInt((ConstValues.maxImageSize + 1) - ConstValues.minImageSize); random.nextIntInRange(min: ConstValues.minImageSize, max: ConstValues.maxImageSize);
final imageUri = _imageUrlGenerator(imageSide: imageSide); final imageUri = _imageUrlGenerator(imageSide: imageSide);
return ImageModel( return ImageModel(
imageIndex: imageIndex, imageIndex: imageIndex,
uri: imageUri, 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) { } on Exception catch (ex, stackTrace) {
handleException(ex, stackTrace); _loggingService.handleException(ex, stackTrace);
return const Iterable.empty(); return const Iterable.empty();
} }
} }
@override
FutureOr<List<ImageModel>> searchImages({
required String searchStr,
required String token,
}) async {
final numberOfResults = random.nextIntInRange(min: 0, max: ConstValues.numberOfImages);
await Future.delayed(Duration(milliseconds: 250 * numberOfResults));
try {
// Create (randomly-bounded) dummy number of images
return Iterable<int>.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( Uri _imageUrlGenerator({required int imageSide}) => Uri(
scheme: ConstValues.httpsScheme, scheme: ConstValues.httpsScheme,
host: ConstValues.backendHost, host: ConstValues.backendHost,

View File

@ -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;
}
}
}

View File

@ -1,8 +1,13 @@
import 'package:mc_gallery/features/core/services/logging_service.dart'; import 'dart:async';
import 'package:mc_gallery/features/home/data/models/image_model.dart';
import 'package:mc_gallery/locator.dart';
import 'package:mc_gallery/features/core/utils/mutex.dart';
import '/features/core/data/extensions/iterable_extensions.dart';
import '/features/core/services/logging_service.dart';
import '/features/home/data/models/image_model.dart';
import '/locator.dart';
import '../abstracts/images_api.dart'; import '../abstracts/images_api.dart';
import '../data/enums/search_option.dart';
/// Handles fetching and storing of Images. /// Handles fetching and storing of Images.
/// ///
@ -21,7 +26,8 @@ class ImagesService {
final LoggingService _loggingService; final LoggingService _loggingService;
late final Iterable<ImageModel> _imageModels; late final Iterable<ImageModel> _imageModels;
Iterable<ImageModel> get imageModels => _imageModels; Iterable<ImageModel> get imageModels => _imageModels.deepCopy;
final Mutex _searchMutex = Mutex();
Future<void> _init() async { Future<void> _init() async {
_loggingService.info('Fetching and creating image models...'); _loggingService.info('Fetching and creating image models...');
@ -40,5 +46,28 @@ class ImagesService {
ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index); ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index);
Future<void> get lastQueryIsCompleted => _searchMutex.lastOperationCompletionAwaiter;
Future<List<ImageModel>> 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(); static ImagesService get locate => Locator.locate();
} }

View File

@ -0,0 +1,43 @@
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),
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),
),
),
),
],
),
),
);
}
}

View File

@ -3,10 +3,17 @@ import 'package:flutter/material.dart';
import '/features/core/data/constants/const_colors.dart'; import '/features/core/data/constants/const_colors.dart';
import '/features/core/data/constants/const_durations.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/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'; import 'gallery_view_model.dart';
part 'downloaded_gallery_view.dart';
part 'search_gallery_view.dart';
class GalleryView extends StatelessWidget { class GalleryView extends StatelessWidget {
const GalleryView({super.key}); const GalleryView({super.key});
@ -18,7 +25,36 @@ class GalleryView extends StatelessWidget {
bodyBuilderWaiter: model.isInitialised, bodyBuilderWaiter: model.isInitialised,
forceInternetCheck: true, forceInternetCheck: true,
appBar: AppBar( appBar: AppBar(
title: Text(model.strings.gallery), centerTitle: true,
primary: true,
title: ValueListenableBuilder<bool>(
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( body: Center(
child: ValueListenableBuilder<bool>( child: ValueListenableBuilder<bool>(
@ -30,33 +66,15 @@ class GalleryView extends StatelessWidget {
onPressed: model.onPromptPressed, onPressed: model.onPromptPressed,
child: Text(model.strings.startLoadingPrompt), child: Text(model.strings.startLoadingPrompt),
) )
: DecoratedBox( : SingleChildScrollView(
decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour), // Using Wrap instead of GridView, to make use of different image sizes
child: SingleChildScrollView( child: ValueListenableBuilder<bool>(
// Using Wrap instead of GridView, to make use of different image sizes valueListenable: model.isSearchingListenable,
child: Wrap( builder: (context, final isSearching, _) => AnimatedSwitcher(
runSpacing: 24, duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
spacing: 8, child: !isSearching
alignment: WrapAlignment.center, ? _DownloadedGalleryView(galleryViewModel: model)
runAlignment: WrapAlignment.center, : _SearchGalleryView(galleryViewModel: model),
crossAxisAlignment: WrapCrossAlignment.center,
children: [
for (final imageModel in model.imageModels)
GestureDetector(
onTap: () => model.pushImageCarouselView(
context,
imageModel: imageModel,
),
child: CachedNetworkImage(
imageUrl: imageModel.uri.toString(),
cacheKey: imageModel.imageIndex.toString(),
progressIndicatorBuilder: (_, __, final progress) =>
CircularProgressIndicator(
value: model.downloadProgressValue(progress: progress),
),
),
),
],
), ),
), ),
), ),
@ -67,3 +85,48 @@ 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<SearchOption>(
valueListenable: galleryViewModel.searchOptionListenable,
builder: (context, final searchOption, _) => Padding(
padding: const EdgeInsets.only(top: 8),
child: DropdownButton<SearchOption>(
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(
decoration: InputDecoration(
hintText: galleryViewModel.strings.searchForImage,
),
onChanged: galleryViewModel.onSearchTermUpdate,
),
),
],
);
}
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.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/services/images_service.dart';
import '/features/home/views/image_carousel/image_carousel_view.dart'; import '/features/home/views/image_carousel/image_carousel_view.dart';
import '/locator.dart'; import '/locator.dart';
import '../../data/enums/search_option.dart';
class GalleryViewModel extends BaseViewModel { class GalleryViewModel extends BaseViewModel {
GalleryViewModel({ GalleryViewModel({
@ -30,6 +33,13 @@ class GalleryViewModel extends BaseViewModel {
final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true); final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true);
ValueListenable<bool> get isDisplayingPressingPrompt => _isDisplayingPressingPrompt; ValueListenable<bool> get isDisplayingPressingPrompt => _isDisplayingPressingPrompt;
final ValueNotifier<bool> _isSearchingNotifier = ValueNotifier(false);
ValueListenable<bool> get isSearchingListenable => _isSearchingNotifier;
final ValueNotifier<SearchOption> _searchOptionNotifier = ValueNotifier(SearchOption.web);
ValueListenable<SearchOption> get searchOptionListenable => _searchOptionNotifier;
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
ValueListenable<List<ImageModel>> get imageSearchResultsListenable => _imageSearchResultsNotifier;
@override @override
Future<void> initialise(bool Function() mounted, [arguments]) async { Future<void> initialise(bool Function() mounted, [arguments]) async {
super.initialise(mounted, arguments); super.initialise(mounted, arguments);
@ -40,6 +50,37 @@ class GalleryViewModel extends BaseViewModel {
super.dispose(); super.dispose();
} }
Future<void> 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<void> get lastQueryResultDone => _imagesService.lastQueryIsCompleted;
void onSearchOptionChanged(SearchOption? option) => _searchOptionNotifier.value = option!;
void onPromptPressed() => _isDisplayingPressingPrompt.value = false; void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
Iterable<ImageModel> get imageModels => _imagesService.imageModels; Iterable<ImageModel> get imageModels => _imagesService.imageModels;

View File

@ -0,0 +1,63 @@
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<List<ImageModel>>(
valueListenable: galleryViewModel.imageSearchResultsListenable,
builder: (context, final resultsImageModels, _) => FutureBuilder(
future: galleryViewModel.lastQueryResultDone,
builder: (context, final snapshot) {
debugPrint(
'--------------------\nBuilding with${resultsImageModels.length} results.\nStatus: ${snapshot.connectionState}\n-------------------');
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,
);
},
),
);
}
}

View File

@ -9,8 +9,8 @@ import '/features/core/data/constants/const_colors.dart';
import '/features/core/data/constants/const_text.dart'; import '/features/core/data/constants/const_text.dart';
import '/features/core/widgets/gap.dart'; import '/features/core/widgets/gap.dart';
import '/features/core/widgets/mcg_scaffold.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 '/features/home/views/image_carousel/image_carousel_view_model.dart';
import '../../../core/widgets/state/view_model_builder.dart';
class ImageCarouselViewArguments { class ImageCarouselViewArguments {
const ImageCarouselViewArguments({required this.imageIndexKey}); const ImageCarouselViewArguments({required this.imageIndexKey});

View File

@ -20,18 +20,29 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary { class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en'; 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); final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{ static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"), "gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
"image": MessageLookupByLibrary.simpleMessage("Image"),
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"), "imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
"imageDetails": MessageLookupByLibrary.simpleMessage( "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?"), "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( "noInternetMessage": MessageLookupByLibrary.simpleMessage(
"Are you sure that you\'re connected to the internet?"), "Are you sure that you\'re connected to the internet?"),
"searchForImage":
MessageLookupByLibrary.simpleMessage("Search for your image"),
"somethingWentWrong": "somethingWentWrong":
MessageLookupByLibrary.simpleMessage("Something went wrong"), MessageLookupByLibrary.simpleMessage("Something went wrong"),
"startLoadingPrompt": "startLoadingPrompt":
MessageLookupByLibrary.simpleMessage("Press me to start loading") MessageLookupByLibrary.simpleMessage("Press me to start loading"),
"web": MessageLookupByLibrary.simpleMessage("Web")
}; };
} }

View File

@ -60,13 +60,23 @@ class Strings {
); );
} }
/// `Image` /// `Image {imageNumber}: size={imageSide}`
String get image { String imageNameFetch(Object imageNumber, Object imageSide) {
return Intl.message( return Intl.message(
'Image', 'Image $imageNumber: size=$imageSide',
name: 'image', name: 'imageNameFetch',
desc: '', 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` /// `Image carousel`
String get imageCarousel { String get imageCarousel {
return Intl.message( return Intl.message(

View File

@ -2,10 +2,14 @@
"@@locale": "en", "@@locale": "en",
"somethingWentWrong": "Something went wrong", "somethingWentWrong": "Something went wrong",
"image": "Image", "imageNameFetch": "Image {imageNumber}: size={imageSide}",
"imageNameSearch": "Search term '{searchStr}' result: Image {imageNumber}",
"gallery": "Gallery", "gallery": "Gallery",
"startLoadingPrompt": "Press me to start loading", "startLoadingPrompt": "Press me to start loading",
"searchForImage": "Search for your image",
"local": "Local",
"web": "Web",
"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?", "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?",