live search

This commit is contained in:
Mehul Ahal 2022-12-23 11:20:46 +01:00
parent 730db86b80
commit ac5531ced3
10 changed files with 231 additions and 33 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,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:provider/provider.dart';
import '../abstracts/base_view_model.dart';
import '../../abstracts/base_view_model.dart';
class ViewModelBuilder<T extends BaseViewModel> extends StatefulWidget {
const ViewModelBuilder({

View File

@ -1,7 +1,7 @@
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 '/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';
/// Handles fetching and storing of Images.
@ -21,7 +21,7 @@ class ImagesService {
final LoggingService _loggingService;
late final Iterable<ImageModel> _imageModels;
Iterable<ImageModel> get imageModels => _imageModels;
Iterable<ImageModel> get imageModels => _imageModels.deepCopy;
Future<void> _init() async {
_loggingService.info('Fetching and creating image models...');
@ -40,5 +40,13 @@ class ImagesService {
ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index);
List<ImageModel> searchLocal({required String imageNamePart}) {
return [];
}
List<ImageModel> searchApi({required String imageNamePart}) {
return [];
}
static ImagesService get locate => Locator.locate();
}

View File

@ -1,10 +1,12 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:mc_gallery/features/core/widgets/gap.dart';
import '/features/core/data/constants/const_colors.dart';
import '/features/core/data/constants/const_durations.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 'gallery_view_model.dart';
class GalleryView extends StatelessWidget {
@ -18,7 +20,39 @@ class GalleryView extends StatelessWidget {
bodyBuilderWaiter: model.isInitialised,
forceInternetCheck: true,
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: IconButton(
icon: MultiValueListenableBuilder(
valueListenables: [
model.isDisplayingPressingPrompt,
model.isSearchingListenable,
],
builder: (context, final values, child) => AnimatedSwitcher(
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
child: !model.isDisplayingPressingPrompt.value
? !model.isSearchingListenable.value
? const Icon(Icons.search)
: const Icon(Icons.close)
: const SizedBox.shrink(),
),
),
onPressed: model.searchPressed,
),
)
],
),
body: Center(
child: ValueListenableBuilder<bool>(
@ -34,29 +68,38 @@ class GalleryView extends StatelessWidget {
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,
),
child: CachedNetworkImage(
imageUrl: imageModel.uri.toString(),
cacheKey: imageModel.imageIndex.toString(),
progressIndicatorBuilder: (_, __, final progress) =>
CircularProgressIndicator(
value: model.downloadProgressValue(progress: progress),
),
),
),
],
child: ValueListenableBuilder<bool>(
valueListenable: model.isSearchingListenable,
builder: (context, final isSearching, _) => AnimatedSwitcher(
duration: ConstDurations.doubleDefaultAnimationDuration,
child: !isSearching
? 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,
),
child: CachedNetworkImage(
imageUrl: imageModel.uri.toString(),
cacheKey: imageModel.imageIndex.toString(),
progressIndicatorBuilder: (_, __, final progress) =>
CircularProgressIndicator(
value:
model.downloadProgressValue(progress: progress),
),
),
),
],
)
: const Placeholder(),
),
),
),
),
@ -67,3 +110,51 @@ class GalleryView extends StatelessWidget {
);
}
}
class _SearchBox extends StatelessWidget {
_SearchBox({
required this.galleryViewModel,
super.key,
});
final GalleryViewModel galleryViewModel;
final TextEditingController _searchController = TextEditingController();
@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(
controller: _searchController,
decoration: InputDecoration(
hintText: galleryViewModel.strings.searchForImage,
),
onChanged: (final searchPart) {},
),
),
],
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:mc_gallery/l10n/generated/l10n.dart';
import '/features/core/abstracts/base_view_model.dart';
import '/features/core/services/logging_service.dart';
@ -30,6 +31,14 @@ class GalleryViewModel extends BaseViewModel {
final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true);
ValueListenable<bool> get isDisplayingPressingPrompt => _isDisplayingPressingPrompt;
final ValueNotifier<bool> _isSearchingNotifier = ValueNotifier(false);
ValueListenable<bool> get isSearchingListenable => _isSearchingNotifier;
final ValueNotifier<SearchOption> _searchOptionNotifier = ValueNotifier(SearchOption.local);
ValueListenable<SearchOption> get searchOptionListenable => _searchOptionNotifier;
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
ValueListenable<List<ImageModel>> get imageSearchResultsNotifier => _imageSearchResultsNotifier;
@override
Future<void> initialise(bool Function() mounted, [arguments]) async {
super.initialise(mounted, arguments);
@ -41,6 +50,8 @@ class GalleryViewModel extends BaseViewModel {
}
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
void searchPressed() => _isSearchingNotifier.value = !_isSearchingNotifier.value;
void onSearchOptionChanged(SearchOption? option) => _searchOptionNotifier.value = option!;
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
@ -57,3 +68,17 @@ class GalleryViewModel extends BaseViewModel {
double? downloadProgressValue({required DownloadProgress progress}) =>
progress.totalSize != null ? progress.downloaded / progress.totalSize! : null;
}
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

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

View File

@ -27,11 +27,15 @@ class MessageLookup extends MessageLookupByLibrary {
"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?"),
"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")
};
}

View File

@ -90,6 +90,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(

View File

@ -6,6 +6,9 @@
"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?",