live search
This commit is contained in:
parent
730db86b80
commit
ac5531ced3
10 changed files with 231 additions and 33 deletions
|
@ -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: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({
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
import '/features/core/data/extensions/iterable_extensions.dart';
|
||||||
import 'package:mc_gallery/features/home/data/models/image_model.dart';
|
import '/features/core/services/logging_service.dart';
|
||||||
import 'package:mc_gallery/locator.dart';
|
import '/features/home/data/models/image_model.dart';
|
||||||
|
import '/locator.dart';
|
||||||
import '../abstracts/images_api.dart';
|
import '../abstracts/images_api.dart';
|
||||||
|
|
||||||
/// Handles fetching and storing of Images.
|
/// Handles fetching and storing of Images.
|
||||||
|
@ -21,7 +21,7 @@ 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;
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_loggingService.info('Fetching and creating image models...');
|
_loggingService.info('Fetching and creating image models...');
|
||||||
|
@ -40,5 +40,13 @@ class ImagesService {
|
||||||
|
|
||||||
ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index);
|
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();
|
static ImagesService get locate => Locator.locate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.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_colors.dart';
|
||||||
import '/features/core/data/constants/const_durations.dart';
|
import '/features/core/data/constants/const_durations.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 'gallery_view_model.dart';
|
import 'gallery_view_model.dart';
|
||||||
|
|
||||||
class GalleryView extends StatelessWidget {
|
class GalleryView extends StatelessWidget {
|
||||||
|
@ -18,7 +20,39 @@ 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: 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(
|
body: Center(
|
||||||
child: ValueListenableBuilder<bool>(
|
child: ValueListenableBuilder<bool>(
|
||||||
|
@ -34,29 +68,38 @@ class GalleryView extends StatelessWidget {
|
||||||
decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour),
|
decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
// Using Wrap instead of GridView, to make use of different image sizes
|
// Using Wrap instead of GridView, to make use of different image sizes
|
||||||
child: Wrap(
|
child: ValueListenableBuilder<bool>(
|
||||||
runSpacing: 24,
|
valueListenable: model.isSearchingListenable,
|
||||||
spacing: 8,
|
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
||||||
alignment: WrapAlignment.center,
|
duration: ConstDurations.doubleDefaultAnimationDuration,
|
||||||
runAlignment: WrapAlignment.center,
|
child: !isSearching
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
? Wrap(
|
||||||
children: [
|
runSpacing: 24,
|
||||||
for (final imageModel in model.imageModels)
|
spacing: 8,
|
||||||
GestureDetector(
|
alignment: WrapAlignment.center,
|
||||||
onTap: () => model.pushImageCarouselView(
|
runAlignment: WrapAlignment.center,
|
||||||
context,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
imageModel: imageModel,
|
children: [
|
||||||
),
|
for (final imageModel in model.imageModels)
|
||||||
child: CachedNetworkImage(
|
GestureDetector(
|
||||||
imageUrl: imageModel.uri.toString(),
|
onTap: () => model.pushImageCarouselView(
|
||||||
cacheKey: imageModel.imageIndex.toString(),
|
context,
|
||||||
progressIndicatorBuilder: (_, __, final progress) =>
|
imageModel: imageModel,
|
||||||
CircularProgressIndicator(
|
),
|
||||||
value: model.downloadProgressValue(progress: progress),
|
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/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';
|
||||||
|
import 'package:mc_gallery/l10n/generated/l10n.dart';
|
||||||
|
|
||||||
import '/features/core/abstracts/base_view_model.dart';
|
import '/features/core/abstracts/base_view_model.dart';
|
||||||
import '/features/core/services/logging_service.dart';
|
import '/features/core/services/logging_service.dart';
|
||||||
|
@ -30,6 +31,14 @@ 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.local);
|
||||||
|
ValueListenable<SearchOption> get searchOptionListenable => _searchOptionNotifier;
|
||||||
|
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
|
||||||
|
ValueListenable<List<ImageModel>> get imageSearchResultsNotifier => _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);
|
||||||
|
@ -41,6 +50,8 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
|
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
|
||||||
|
void searchPressed() => _isSearchingNotifier.value = !_isSearchingNotifier.value;
|
||||||
|
void onSearchOptionChanged(SearchOption? option) => _searchOptionNotifier.value = option!;
|
||||||
|
|
||||||
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
||||||
|
|
||||||
|
@ -57,3 +68,17 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
double? downloadProgressValue({required DownloadProgress progress}) =>
|
double? downloadProgressValue({required DownloadProgress progress}) =>
|
||||||
progress.totalSize != null ? progress.downloaded / progress.totalSize! : null;
|
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/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});
|
||||||
|
|
|
@ -27,11 +27,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
"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?"),
|
||||||
|
"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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`
|
/// `Image carousel`
|
||||||
String get imageCarousel {
|
String get imageCarousel {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
"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?",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue