Favourites
This commit is contained in:
parent
6a84a9bef0
commit
1747ab0245
23 changed files with 469 additions and 67 deletions
|
@ -1,13 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import '../data/dtos/image_model_dto.dart';
|
||||
|
||||
/// Interface for implementing image-fetching strategies, specific to a resource location on the internet.
|
||||
///
|
||||
/// Since I used a site that was more obscure than the ones in the examples, this (otherwise pointless
|
||||
/// and convoluting) interface is for adding a bit of flexibility to change strategy to some other site.
|
||||
abstract class ImagesApi {
|
||||
FutureOr<Iterable<Map<String, dynamic>>> fetchImageUri({required String token});
|
||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token});
|
||||
|
||||
FutureOr<Iterable<Map<String, dynamic>>> searchImages({
|
||||
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||
required String searchStr,
|
||||
required String token,
|
||||
});
|
||||
|
|
|
@ -7,18 +7,19 @@ 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';
|
||||
import '../data/dtos/image_model_dto.dart';
|
||||
|
||||
class UnsplashImagesApi implements ImagesApi {
|
||||
final LoggingService _loggingService = LoggingService.locate;
|
||||
final random = Random();
|
||||
|
||||
@override
|
||||
FutureOr<Iterable<Map<String, dynamic>>> fetchImageUri({required String token}) async {
|
||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token}) async {
|
||||
// Dummy fetching delay emulation
|
||||
await Future.delayed(const Duration(
|
||||
milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages));
|
||||
|
||||
final Iterable<Map<String, dynamic>> fetchedImageModelDtos;
|
||||
try {
|
||||
// Create fixed number of images
|
||||
final dummyImageModels =
|
||||
|
@ -29,7 +30,7 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
|
||||
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||
|
||||
return ImageModel(
|
||||
return ImageModelDTO(
|
||||
imageIndex: imageIndex,
|
||||
uri: imageUri,
|
||||
// Custom dummy name for the image
|
||||
|
@ -39,15 +40,19 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
});
|
||||
|
||||
// Emulating serialization
|
||||
return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||
fetchedImageModelDtos = dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||
} on Exception catch (ex, stackTrace) {
|
||||
_loggingService.handleException(ex, stackTrace);
|
||||
return const Iterable.empty();
|
||||
}
|
||||
|
||||
// Emulating deserialization
|
||||
return fetchedImageModelDtos
|
||||
.map((final emulatedModelSerialized) => ImageModelDTO.fromJson(emulatedModelSerialized));
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Iterable<Map<String, dynamic>>> searchImages({
|
||||
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||
required String searchStr,
|
||||
required String token,
|
||||
}) async {
|
||||
|
@ -57,6 +62,7 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
await Future.delayed(
|
||||
Duration(milliseconds: ConstValues.defaultEmulatedLatencyMillis * numberOfResults));
|
||||
|
||||
final Iterable<Map<String, dynamic>> searchImageModelDtos;
|
||||
try {
|
||||
// Create (randomly-bounded) dummy number of images
|
||||
final dummyImageModels = Iterable<int>.generate(numberOfResults).map((final imageIndex) {
|
||||
|
@ -66,7 +72,7 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
|
||||
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||
|
||||
return ImageModel(
|
||||
return ImageModelDTO(
|
||||
imageIndex: imageIndex,
|
||||
uri: imageUri,
|
||||
// Custom dummy name for the image
|
||||
|
@ -75,11 +81,14 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
});
|
||||
|
||||
// Emulating serialization
|
||||
return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||
searchImageModelDtos = dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||
} on Exception catch (ex, stackTrace) {
|
||||
_loggingService.handleException(ex, stackTrace);
|
||||
return List.empty();
|
||||
}
|
||||
|
||||
return searchImageModelDtos
|
||||
.map((final emulatedModelSerialized) => ImageModelDTO.fromJson(emulatedModelSerialized));
|
||||
}
|
||||
|
||||
Uri _imageUrlGenerator({required int imageSide}) => Uri(
|
||||
|
|
26
lib/features/home/data/dtos/image_model_dto.dart
Normal file
26
lib/features/home/data/dtos/image_model_dto.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'image_model_dto.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class ImageModelDTO {
|
||||
const ImageModelDTO({
|
||||
required this.uri,
|
||||
required this.imageIndex,
|
||||
required this.imageName,
|
||||
});
|
||||
|
||||
/// An image's target [Uri].
|
||||
///
|
||||
/// Storing an image's [ByteData] is more expensive, memory-wise.
|
||||
final Uri uri;
|
||||
|
||||
/// A unique identifier that can be used for indexing the image.
|
||||
final int imageIndex;
|
||||
|
||||
/// Given name of the image.
|
||||
final String imageName;
|
||||
|
||||
factory ImageModelDTO.fromJson(Map<String, dynamic> json) => _$ImageModelDTOFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ImageModelDTOToJson(this);
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'image_model.dart';
|
||||
part of 'image_model_dto.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ImageModel _$ImageModelFromJson(Map<String, dynamic> json) => ImageModel(
|
||||
ImageModelDTO _$ImageModelDTOFromJson(Map<String, dynamic> json) =>
|
||||
ImageModelDTO(
|
||||
uri: Uri.parse(json['uri'] as String),
|
||||
imageIndex: json['imageIndex'] as int,
|
||||
imageName: json['imageName'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ImageModelToJson(ImageModel instance) =>
|
||||
Map<String, dynamic> _$ImageModelDTOToJson(ImageModelDTO instance) =>
|
||||
<String, dynamic>{
|
||||
'uri': instance.uri.toString(),
|
||||
'imageIndex': instance.imageIndex,
|
|
@ -1,13 +1,11 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../dtos/image_model_dto.dart';
|
||||
|
||||
part 'image_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class ImageModel {
|
||||
const ImageModel({
|
||||
required this.uri,
|
||||
required this.imageIndex,
|
||||
required this.imageName,
|
||||
required this.isFavourite,
|
||||
});
|
||||
|
||||
/// An image's target [Uri].
|
||||
|
@ -21,8 +19,31 @@ class ImageModel {
|
|||
/// Given name of the image.
|
||||
final String imageName;
|
||||
|
||||
factory ImageModel.fromJson(Map<String, dynamic> json) => _$ImageModelFromJson(json);
|
||||
/// Whether the image was 'Starred' ot not.
|
||||
final bool isFavourite;
|
||||
|
||||
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
|
||||
Map<String, dynamic> toJson() => _$ImageModelToJson(this);
|
||||
factory ImageModel.fromDto({
|
||||
required ImageModelDTO imageModelDto,
|
||||
required bool isFavourite,
|
||||
}) =>
|
||||
ImageModel(
|
||||
uri: imageModelDto.uri,
|
||||
imageIndex: imageModelDto.imageIndex,
|
||||
imageName: imageModelDto.imageName,
|
||||
isFavourite: isFavourite,
|
||||
);
|
||||
|
||||
ImageModel copyWith({
|
||||
Uri? uri,
|
||||
int? imageIndex,
|
||||
String? imageName,
|
||||
bool? isFavourite,
|
||||
}) {
|
||||
return ImageModel(
|
||||
uri: uri ?? this.uri,
|
||||
imageIndex: imageIndex ?? this.imageIndex,
|
||||
imageName: imageName ?? this.imageName,
|
||||
isFavourite: isFavourite ?? this.isFavourite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,24 @@ import 'dart:ui';
|
|||
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mc_gallery/features/core/services/app_lifecycle_service.dart';
|
||||
import 'package:mc_gallery/features/core/services/local_storage_service.dart';
|
||||
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
||||
import 'package:mc_gallery/locator.dart';
|
||||
|
||||
class ImageCacheManagerService with LoggingService {
|
||||
ImageCacheManagerService({
|
||||
required AppLifecycleService appLifecycleService,
|
||||
}) : _appLifecycleService = appLifecycleService {
|
||||
ImageCacheManagerService(
|
||||
{required AppLifecycleService appLifecycleService,
|
||||
required LocalStorageService localStorageService})
|
||||
: _appLifecycleService = appLifecycleService,
|
||||
_localStorageService = localStorageService {
|
||||
_init();
|
||||
}
|
||||
|
||||
final AppLifecycleService _appLifecycleService;
|
||||
final LocalStorageService _localStorageService;
|
||||
final _cacheManager = DefaultCacheManager();
|
||||
|
||||
Future<void> emptyCache() async => await DefaultCacheManager().emptyCache();
|
||||
Future<void> emptyCache() async => await _cacheManager.emptyCache();
|
||||
|
||||
Future<void> _init() async {
|
||||
_appLifecycleService.addListener(
|
||||
|
@ -27,7 +32,8 @@ class ImageCacheManagerService with LoggingService {
|
|||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
info('Discarding cached images');
|
||||
await DefaultCacheManager().emptyCache();
|
||||
await _cacheManager.emptyCache();
|
||||
_localStorageService.resetFavourites();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:mc_gallery/features/core/data/extensions/string_extensions.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mc_gallery/features/home/data/dtos/image_model_dto.dart';
|
||||
|
||||
import '/features/core/data/constants/const_sorters.dart';
|
||||
import '/features/core/data/extensions/iterable_extensions.dart';
|
||||
import '/features/core/data/extensions/map_extensions.dart';
|
||||
import '/features/core/data/extensions/string_extensions.dart';
|
||||
import '/features/core/services/local_storage_service.dart';
|
||||
import '/features/core/services/logging_service.dart';
|
||||
import '/features/core/utils/mutex.dart';
|
||||
import '/locator.dart';
|
||||
|
@ -19,16 +23,19 @@ import '../data/models/image_model.dart';
|
|||
class ImagesService {
|
||||
ImagesService({
|
||||
required ImagesApi imagesApi,
|
||||
required LocalStorageService localStorageService,
|
||||
required LoggingService loggingService,
|
||||
}) : _imagesApi = imagesApi,
|
||||
_localStorageService = localStorageService,
|
||||
_loggingService = loggingService {
|
||||
_init();
|
||||
}
|
||||
|
||||
final ImagesApi _imagesApi;
|
||||
final LocalStorageService _localStorageService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
late final Map<String, ImageModel> _imageModels;
|
||||
late final LinkedHashMap<String, ImageModel> _imageModels;
|
||||
Iterable<ImageModel> get imageModels => _imageModels.values.deepCopy;
|
||||
|
||||
final Mutex _searchMutex = Mutex();
|
||||
|
@ -41,11 +48,37 @@ class ImagesService {
|
|||
|
||||
Future<void> _init() async {
|
||||
_loggingService.info('Fetching and creating image models...');
|
||||
_imageModels = {
|
||||
for (final imageModel in (await _imagesApi.fetchImageUri(token: ''))
|
||||
.map((final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized)))
|
||||
imageModel.imageName: imageModel
|
||||
};
|
||||
|
||||
final fetchedImageModelDtos = await _imagesApi.fetchImageUri(token: '');
|
||||
final favouritesStatuses = _localStorageService.storedFavouritesStates;
|
||||
|
||||
// Prefill from stored values
|
||||
if (favouritesStatuses.isNotEmpty) {
|
||||
_loggingService.good('Found favourites statuses on device -> Prefilling');
|
||||
assert(fetchedImageModelDtos.length == favouritesStatuses.length);
|
||||
_imageModels = LinkedHashMap.of({
|
||||
for (final pair in IterableZip([fetchedImageModelDtos, favouritesStatuses]))
|
||||
(pair[0] as ImageModelDTO).imageName: ImageModel.fromDto(
|
||||
imageModelDto: pair[0] as ImageModelDTO,
|
||||
isFavourite: pair[1] as bool,
|
||||
)
|
||||
});
|
||||
|
||||
// Set to false and create the stored values
|
||||
} else {
|
||||
_loggingService.good('NO favourites statuses found -> creating new');
|
||||
_imageModels = LinkedHashMap.of({
|
||||
for (final fetchedImageModelDto in fetchedImageModelDtos)
|
||||
fetchedImageModelDto.imageName: ImageModel.fromDto(
|
||||
imageModelDto: fetchedImageModelDto,
|
||||
isFavourite: false,
|
||||
)
|
||||
});
|
||||
|
||||
_localStorageService.initNewFavourites(
|
||||
newValues: _imageModels.values.map((final imageModel) => imageModel.isFavourite),
|
||||
);
|
||||
}
|
||||
|
||||
_imageModels.isNotEmpty
|
||||
? _loggingService.good("Created ${_imageModels.length} images' models")
|
||||
|
@ -89,14 +122,20 @@ class ImagesService {
|
|||
..sort((final a, final b) =>
|
||||
ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b))
|
||||
..reversed;
|
||||
|
||||
return _imageModels.valuesByKeys(keys: rankedKeys).toList(growable: false);
|
||||
|
||||
case SearchOption.web:
|
||||
return (await _imagesApi.searchImages(
|
||||
searchStr: imageNamePart,
|
||||
token: '',
|
||||
))
|
||||
.map(
|
||||
(final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized))
|
||||
(final imageModelDto) => ImageModel.fromDto(
|
||||
imageModelDto: imageModelDto,
|
||||
isFavourite: false,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
} finally {
|
||||
|
@ -105,5 +144,19 @@ class ImagesService {
|
|||
});
|
||||
}
|
||||
|
||||
void updateImageFavouriteStatus({
|
||||
required ImageModel imageModel,
|
||||
required bool newFavouriteStatus,
|
||||
}) {
|
||||
_imageModels.updateValueAt(
|
||||
valueIndex: imageModel.imageIndex,
|
||||
newValue: imageModel.copyWith(isFavourite: newFavouriteStatus),
|
||||
);
|
||||
|
||||
//todo(mehul): Consider adding an update listener to _imageModels, sync with _localStorageService
|
||||
_localStorageService.updateFavourite(
|
||||
index: imageModel.imageIndex, newValue: newFavouriteStatus);
|
||||
}
|
||||
|
||||
static ImagesService get locate => Locator.locate();
|
||||
}
|
||||
|
|
|
@ -15,30 +15,108 @@ class _DownloadedGalleryView extends StatelessWidget {
|
|||
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: ValueListenableBuilder<bool>(
|
||||
valueListenable: galleryViewModel.isViewingFavouriteListenable,
|
||||
builder: (context, final isViewingFavourites, _) => !isViewingFavourites
|
||||
? Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final imageModel in galleryViewModel.imageModels)
|
||||
_StarrableImage(
|
||||
key: ValueKey(imageModel.imageIndex),
|
||||
imageModel: imageModel,
|
||||
galleryViewModel: galleryViewModel,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final favouriteImageModel in galleryViewModel.favouriteImageModels)
|
||||
_StarrableImage(
|
||||
key: ValueKey(favouriteImageModel.imageIndex),
|
||||
imageModel: favouriteImageModel,
|
||||
galleryViewModel: galleryViewModel,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageModel.uri.toString(),
|
||||
cacheKey: imageModel.imageIndex.toString(),
|
||||
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
|
||||
value: galleryViewModel.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StarrableImage extends StatefulWidget {
|
||||
const _StarrableImage({
|
||||
required this.galleryViewModel,
|
||||
required this.imageModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final GalleryViewModel galleryViewModel;
|
||||
final ImageModel imageModel;
|
||||
|
||||
@override
|
||||
State<_StarrableImage> createState() => _StarrableImageState();
|
||||
}
|
||||
|
||||
class _StarrableImageState extends State<_StarrableImage> {
|
||||
late bool isMarkedFavourite;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
isMarkedFavourite = widget.imageModel.isFavourite;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: Alignment.topRight.add(const Alignment(0.2, -0.2)),
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => widget.galleryViewModel.pushImageCarouselView(
|
||||
context,
|
||||
imageModel: widget.imageModel,
|
||||
),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageModel.uri.toString(),
|
||||
cacheKey: widget.imageModel.imageIndex.toString(),
|
||||
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
|
||||
value: widget.galleryViewModel.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: isMarkedFavourite
|
||||
? ConstMedia.buildIcon(
|
||||
ConstMedia.favStarFilled,
|
||||
width: 16,
|
||||
height: 16,
|
||||
)
|
||||
: ConstMedia.buildIcon(
|
||||
ConstMedia.favStarOutline,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
onTap: () {
|
||||
widget.galleryViewModel.updateImageFavouriteStatus(
|
||||
imageModel: widget.imageModel,
|
||||
newFavouriteStatus: !isMarkedFavourite,
|
||||
);
|
||||
setState(() => isMarkedFavourite = !isMarkedFavourite);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mc_gallery/features/core/data/constants/const_media.dart';
|
||||
|
||||
import '/features/core/data/constants/const_colors.dart';
|
||||
import '/features/core/data/constants/const_durations.dart';
|
||||
|
@ -80,9 +81,21 @@ class GalleryView extends StatelessWidget {
|
|||
valueListenable: model.isSearchingListenable,
|
||||
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
||||
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
||||
child: !isSearching
|
||||
? _DownloadedGalleryView(galleryViewModel: model)
|
||||
: _SearchGalleryView(galleryViewModel: model),
|
||||
child: Column(
|
||||
children: [
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: model.isViewingFavouriteListenable,
|
||||
builder: (context, final isViewingFavourites, child) =>
|
||||
Switch(
|
||||
value: isViewingFavourites,
|
||||
onChanged: model.onFavouriteViewChange,
|
||||
),
|
||||
),
|
||||
!isSearching
|
||||
? _DownloadedGalleryView(galleryViewModel: model)
|
||||
: _SearchGalleryView(galleryViewModel: model),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mc_gallery/features/core/data/extensions/value_notifier_extensions.dart';
|
||||
|
||||
import '/features/core/abstracts/base_view_model.dart';
|
||||
import '/features/core/services/logging_service.dart';
|
||||
|
@ -27,6 +28,7 @@ class GalleryViewModel extends BaseViewModel {
|
|||
|
||||
final ImagesService _imagesService;
|
||||
final NavigationService _navigationService;
|
||||
//todo(mehul): Use to implement pull-to-refresh or an extra widget
|
||||
final ImageCacheManagerService _imageCacheManagerService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
|
@ -40,6 +42,9 @@ class GalleryViewModel extends BaseViewModel {
|
|||
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
|
||||
ValueListenable<List<ImageModel>> get imageSearchResultsListenable => _imageSearchResultsNotifier;
|
||||
|
||||
final ValueNotifier<bool> _isViewingFavouriteNotifier = ValueNotifier(false);
|
||||
ValueListenable<bool> get isViewingFavouriteListenable => _isViewingFavouriteNotifier;
|
||||
|
||||
@override
|
||||
Future<void> initialise(bool Function() mounted, [arguments]) async {
|
||||
super.initialise(mounted, arguments);
|
||||
|
@ -80,7 +85,7 @@ class GalleryViewModel extends BaseViewModel {
|
|||
_loggingService.info('Clearing of results on view mode change');
|
||||
}
|
||||
|
||||
_isSearchingNotifier.value = !_isSearchingNotifier.value;
|
||||
_isSearchingNotifier.flipValue();
|
||||
}
|
||||
|
||||
Future<void> get lastQueryResultDone => _imagesService.lastQueryIsCompleted;
|
||||
|
@ -92,9 +97,24 @@ class GalleryViewModel extends BaseViewModel {
|
|||
_imageSearchResultsNotifier.value = [];
|
||||
_loggingService.info('Cleared resultsw from view');
|
||||
|
||||
//todo(mehul): Either redo search or force user to type in by clearing field
|
||||
//todo(mehul): Either redo search or force user to type in new (trigger) by clearing field
|
||||
}
|
||||
|
||||
void onFavouriteViewChange(bool newValue) => _isViewingFavouriteNotifier.value = newValue;
|
||||
|
||||
void updateImageFavouriteStatus({
|
||||
required ImageModel imageModel,
|
||||
required bool newFavouriteStatus,
|
||||
}) {
|
||||
_imagesService.updateImageFavouriteStatus(
|
||||
imageModel: imageModel,
|
||||
newFavouriteStatus: newFavouriteStatus,
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<ImageModel> get favouriteImageModels =>
|
||||
imageModels.where((final imageModel) => imageModel.isFavourite);
|
||||
|
||||
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
|
||||
|
||||
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:mc_gallery/features/home/data/models/image_model.dart';
|
||||
|
||||
import '/features/core/data/constants/const_colors.dart';
|
||||
import '/features/core/data/constants/const_text.dart';
|
||||
|
@ -11,6 +10,7 @@ import '/features/core/widgets/gap.dart';
|
|||
import '/features/core/widgets/mcg_scaffold.dart';
|
||||
import '/features/home/views/image_carousel/image_carousel_view_model.dart';
|
||||
import '../../../core/widgets/state/view_model_builder.dart';
|
||||
import '../../data/models/image_model.dart';
|
||||
|
||||
class ImageCarouselViewArguments {
|
||||
const ImageCarouselViewArguments({required this.imageIndexKey});
|
||||
|
|
|
@ -5,10 +5,10 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|||
import '/features/core/abstracts/base_view_model.dart';
|
||||
import '/features/core/services/logging_service.dart';
|
||||
import '/features/core/services/navigation_service.dart';
|
||||
import '/features/home/data/models/image_model.dart';
|
||||
import '/features/home/services/images_service.dart';
|
||||
import '/features/home/views/image_carousel/image_carousel_view.dart';
|
||||
import '/locator.dart';
|
||||
import '../../data/models/image_model.dart';
|
||||
|
||||
class ImageCarouselViewModel extends BaseViewModel {
|
||||
ImageCarouselViewModel({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue