live search
This commit is contained in:
parent
730db86b80
commit
ac5531ced3
|
@ -0,0 +1,3 @@
|
|||
extension IterableExtensions<T> on Iterable<T> {
|
||||
Iterable<T> get deepCopy => toList(growable: false);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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({
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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")
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?",
|
||||
|
|
Loading…
Reference in New Issue