Compare commits
2 commits
56a9e3f421
...
84ec8304cd
Author | SHA1 | Date | |
---|---|---|---|
84ec8304cd | |||
68d7f70ded |
25 changed files with 482 additions and 77 deletions
41
assets/icons/star_filled.svg
Normal file
41
assets/icons/star_filled.svg
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVGRepo, www.svgrepo.com, Transformed by: SVGRepo Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 47.94 47.94" xml:space="preserve">
|
||||||
|
<path style="fill:#ED8A19;" d="M26.285,2.486l5.407,10.956c0.376,0.762,1.103,1.29,1.944,1.412l12.091,1.757
|
||||||
|
c2.118,0.308,2.963,2.91,1.431,4.403l-8.749,8.528c-0.608,0.593-0.886,1.448-0.742,2.285l2.065,12.042
|
||||||
|
c0.362,2.109-1.852,3.717-3.746,2.722l-10.814-5.685c-0.752-0.395-1.651-0.395-2.403,0l-10.814,5.685
|
||||||
|
c-1.894,0.996-4.108-0.613-3.746-2.722l2.065-12.042c0.144-0.837-0.134-1.692-0.742-2.285l-8.749-8.528
|
||||||
|
c-1.532-1.494-0.687-4.096,1.431-4.403l12.091-1.757c0.841-0.122,1.568-0.65,1.944-1.412l5.407-10.956
|
||||||
|
C22.602,0.567,25.338,0.567,26.285,2.486z"/>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1 KiB |
1
assets/icons/star_outline.svg
Normal file
1
assets/icons/star_outline.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 117.42"><path d="M66.71 3.55L81.1 37.26l36.58 3.28v-.01c1.55.13 2.91.89 3.85 2.01a5.663 5.663 0 011.32 4.13v.01a5.673 5.673 0 01-1.69 3.57c-.12.13-.25.25-.39.36L93.25 74.64l8.19 35.83c.35 1.53.05 3.06-.73 4.29a5.652 5.652 0 01-3.54 2.52l-.14.03c-.71.14-1.43.15-2.12.02v.01c-.75-.13-1.47-.42-2.11-.84l-.05-.03-31.3-18.71-31.55 18.86a5.664 5.664 0 01-7.79-1.96c-.38-.64-.62-1.33-.73-2.02-.1-.63-.09-1.27.02-1.89.02-.13.04-.27.08-.4l8.16-35.7c-9.24-8.07-18.74-16.1-27.83-24.3l-.08-.08a5.64 5.64 0 01-1.72-3.7c-.1-1.45.36-2.93 1.4-4.12l.12-.13.08-.08a5.668 5.668 0 013.77-1.72h.06l36.34-3.26 14.44-33.8c.61-1.44 1.76-2.5 3.11-3.05 1.35-.54 2.9-.57 4.34.04.69.29 1.3.71 1.8 1.22.53.53.94 1.15 1.22 1.82l.02.06zm10.19 37.2L61.85 5.51a.42.42 0 00-.09-.14.42.42 0 00-.14-.09.427.427 0 00-.35 0c-.1.04-.19.12-.24.24L45.98 40.75c-.37.86-1.18 1.49-2.18 1.58l-37.9 3.4c-.08.01-.16.02-.24.02-.06 0-.13.02-.18.05-.03.01-.05.03-.07.05l-.1.12c-.05.08-.07.17-.06.26.01.09.04.18.09.25.06.05.13.11.19.17l28.63 25c.77.61 1.17 1.62.94 2.65l-8.51 37.22-.03.14c-.01.06-.02.12-.01.17a.454.454 0 00.33.36c.12.03.24.02.34-.04l32.85-19.64c.8-.5 1.85-.54 2.72-.02L95.43 112c.08.04.16.09.24.14.05.03.1.05.16.06v.01c.04.01.09.01.14 0l.04-.01c.12-.03.22-.1.28-.2.06-.09.08-.21.05-.33L87.8 74.28a2.6 2.6 0 01.83-2.55l28.86-25.2c.04-.03.07-.08.1-.13.02-.04.03-.1.04-.17a.497.497 0 00-.09-.33.48.48 0 00-.3-.15v-.01c-.01 0-.03 0-.03-.01l-37.97-3.41c-1-.01-1.93-.6-2.34-1.57z" fill="#ffcf00"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,14 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
|
|
||||||
import '/l10n/generated/l10n.dart';
|
import '/l10n/generated/l10n.dart';
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
|
|
||||||
abstract class AppSetup {
|
abstract class AppSetup {
|
||||||
// TODO: When locator is properly refactored we should not have to use these stub methods for testing
|
|
||||||
static Future<void> initialise() async {
|
static Future<void> initialise() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ abstract class AppSetup {
|
||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await Hive.initFlutter();
|
||||||
|
|
||||||
await Locator.setup();
|
await Locator.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
lib/features/core/data/constants/const_media.dart
Normal file
26
lib/features/core/data/constants/const_media.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
abstract class ConstMedia {
|
||||||
|
static const String favStarFilled = 'assets/icons/star_filled.svg';
|
||||||
|
static const String favStarOutline = 'assets/icons/star_outline.svg';
|
||||||
|
|
||||||
|
static SvgPicture buildIcon(
|
||||||
|
String iconReference, {
|
||||||
|
Color? color,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
BoxFit fit = BoxFit.contain,
|
||||||
|
Clip clipBehavior = Clip.hardEdge,
|
||||||
|
Alignment alignment = Alignment.center,
|
||||||
|
}) =>
|
||||||
|
SvgPicture.asset(
|
||||||
|
iconReference,
|
||||||
|
color: color,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
alignment: alignment,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,17 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
extension MapExtensions<A, B> on Map<A, B> {
|
extension MapExtensions<A, B> on Map<A, B> {
|
||||||
Map<A, B> get deepCopy => {...this};
|
Map<A, B> get deepCopy => {...this};
|
||||||
|
|
||||||
/// Returns the values of a [Map] at given [keys] indices.
|
/// Returns the values of a [Map] at given [keys] indices.
|
||||||
Iterable<B> valuesByKeys({required Iterable<A> keys}) => keys.map((final key) => this[key]!);
|
Iterable<B> valuesByKeys({required Iterable<A> keys}) => keys.map((final key) => this[key]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LinkedHashMapExtensions<A, B> on LinkedHashMap<A, B> {
|
||||||
|
/// Updated the value at [valueIndex] to [newValue], in addition to preserving the order.
|
||||||
|
void updateValueAt({
|
||||||
|
required int valueIndex,
|
||||||
|
required B newValue,
|
||||||
|
}) =>
|
||||||
|
this[keys.toList()[valueIndex]] = newValue;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
extension ValueNotifierBoolExtensions on ValueNotifier<bool> {
|
||||||
|
void flipValue() => value = !value;
|
||||||
|
}
|
47
lib/features/core/services/local_storage_service.dart
Normal file
47
lib/features/core/services/local_storage_service.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
||||||
|
import 'package:mc_gallery/locator.dart';
|
||||||
|
|
||||||
|
class LocalStorageService {
|
||||||
|
LocalStorageService() {
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
final LoggingService _loggingService = LoggingService.locate;
|
||||||
|
|
||||||
|
static const String _userBoxKey = 'userBoxKey';
|
||||||
|
|
||||||
|
late final Box<bool> _userBox;
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
_userBox = await Hive.openBox(_userBoxKey);
|
||||||
|
|
||||||
|
Locator.instance().signalReady(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<bool> get storedFavouritesStates => _userBox.values;
|
||||||
|
|
||||||
|
void initNewFavourites({required Iterable<bool> newValues}) {
|
||||||
|
_userBox.addAll(newValues);
|
||||||
|
_loggingService.info('Adding new favourites value');
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFavourite({
|
||||||
|
required index,
|
||||||
|
required bool newValue,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
_userBox.putAt(index, newValue);
|
||||||
|
_loggingService.good('Successfully updated favourite status at $index -> $newValue');
|
||||||
|
} on Exception catch (ex, stackTrace) {
|
||||||
|
_loggingService.handleException(ex, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetFavourites() {
|
||||||
|
_userBox.clear();
|
||||||
|
_loggingService.info('Cleared favourites table');
|
||||||
|
}
|
||||||
|
|
||||||
|
static LocalStorageService get locate => Locator.locate();
|
||||||
|
}
|
|
@ -25,7 +25,8 @@ class LoggingService {
|
||||||
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get fine => _talker.fine;
|
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get fine => _talker.fine;
|
||||||
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get good => _talker.good;
|
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get good => _talker.good;
|
||||||
|
|
||||||
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get info => _talker.info;
|
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get info =>
|
||||||
|
_talker.verbose;
|
||||||
void Function(dynamic msg, [Object exception, StackTrace stackTrace]) get warning =>
|
void Function(dynamic msg, [Object exception, StackTrace stackTrace]) get warning =>
|
||||||
_talker.warning;
|
_talker.warning;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import '../data/dtos/image_model_dto.dart';
|
||||||
|
|
||||||
/// Interface for implementing image-fetching strategies, specific to a resource location on the internet.
|
/// 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
|
/// 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.
|
/// 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<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 searchStr,
|
||||||
required String token,
|
required String token,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,18 +7,19 @@ import '/features/core/services/logging_service.dart';
|
||||||
import '/l10n/generated/l10n.dart';
|
import '/l10n/generated/l10n.dart';
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
import '../abstracts/images_api.dart';
|
import '../abstracts/images_api.dart';
|
||||||
import '../data/models/image_model.dart';
|
import '../data/dtos/image_model_dto.dart';
|
||||||
|
|
||||||
class UnsplashImagesApi implements ImagesApi {
|
class UnsplashImagesApi implements ImagesApi {
|
||||||
final LoggingService _loggingService = LoggingService.locate;
|
final LoggingService _loggingService = LoggingService.locate;
|
||||||
final random = Random();
|
final random = Random();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Iterable<Map<String, dynamic>>> fetchImageUri({required String token}) async {
|
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token}) async {
|
||||||
// Dummy fetching delay emulation
|
// Dummy fetching delay emulation
|
||||||
await Future.delayed(const Duration(
|
await Future.delayed(const Duration(
|
||||||
milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages));
|
milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages));
|
||||||
|
|
||||||
|
final Iterable<Map<String, dynamic>> fetchedImageModelDtos;
|
||||||
try {
|
try {
|
||||||
// Create fixed number of images
|
// Create fixed number of images
|
||||||
final dummyImageModels =
|
final dummyImageModels =
|
||||||
|
@ -29,7 +30,7 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
|
|
||||||
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||||
|
|
||||||
return ImageModel(
|
return ImageModelDTO(
|
||||||
imageIndex: imageIndex,
|
imageIndex: imageIndex,
|
||||||
uri: imageUri,
|
uri: imageUri,
|
||||||
// Custom dummy name for the image
|
// Custom dummy name for the image
|
||||||
|
@ -39,15 +40,19 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emulating serialization
|
// Emulating serialization
|
||||||
return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
fetchedImageModelDtos = dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||||
} on Exception catch (ex, stackTrace) {
|
} on Exception catch (ex, stackTrace) {
|
||||||
_loggingService.handleException(ex, stackTrace);
|
_loggingService.handleException(ex, stackTrace);
|
||||||
return const Iterable.empty();
|
return const Iterable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emulating deserialization
|
||||||
|
return fetchedImageModelDtos
|
||||||
|
.map((final emulatedModelSerialized) => ImageModelDTO.fromJson(emulatedModelSerialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Iterable<Map<String, dynamic>>> searchImages({
|
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||||
required String searchStr,
|
required String searchStr,
|
||||||
required String token,
|
required String token,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -57,6 +62,7 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
await Future.delayed(
|
await Future.delayed(
|
||||||
Duration(milliseconds: ConstValues.defaultEmulatedLatencyMillis * numberOfResults));
|
Duration(milliseconds: ConstValues.defaultEmulatedLatencyMillis * numberOfResults));
|
||||||
|
|
||||||
|
final Iterable<Map<String, dynamic>> searchImageModelDtos;
|
||||||
try {
|
try {
|
||||||
// Create (randomly-bounded) dummy number of images
|
// Create (randomly-bounded) dummy number of images
|
||||||
final dummyImageModels = Iterable<int>.generate(numberOfResults).map((final imageIndex) {
|
final dummyImageModels = Iterable<int>.generate(numberOfResults).map((final imageIndex) {
|
||||||
|
@ -66,7 +72,7 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
|
|
||||||
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||||
|
|
||||||
return ImageModel(
|
return ImageModelDTO(
|
||||||
imageIndex: imageIndex,
|
imageIndex: imageIndex,
|
||||||
uri: imageUri,
|
uri: imageUri,
|
||||||
// Custom dummy name for the image
|
// Custom dummy name for the image
|
||||||
|
@ -75,11 +81,14 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emulating serialization
|
// Emulating serialization
|
||||||
return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
searchImageModelDtos = dummyImageModels.map((final dummyModel) => dummyModel.toJson());
|
||||||
} on Exception catch (ex, stackTrace) {
|
} on Exception catch (ex, stackTrace) {
|
||||||
_loggingService.handleException(ex, stackTrace);
|
_loggingService.handleException(ex, stackTrace);
|
||||||
return List.empty();
|
return List.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return searchImageModelDtos
|
||||||
|
.map((final emulatedModelSerialized) => ImageModelDTO.fromJson(emulatedModelSerialized));
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri _imageUrlGenerator({required int imageSide}) => Uri(
|
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
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'image_model.dart';
|
part of 'image_model_dto.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
ImageModel _$ImageModelFromJson(Map<String, dynamic> json) => ImageModel(
|
ImageModelDTO _$ImageModelDTOFromJson(Map<String, dynamic> json) =>
|
||||||
|
ImageModelDTO(
|
||||||
uri: Uri.parse(json['uri'] as String),
|
uri: Uri.parse(json['uri'] as String),
|
||||||
imageIndex: json['imageIndex'] as int,
|
imageIndex: json['imageIndex'] as int,
|
||||||
imageName: json['imageName'] as String,
|
imageName: json['imageName'] as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ImageModelToJson(ImageModel instance) =>
|
Map<String, dynamic> _$ImageModelDTOToJson(ImageModelDTO instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'uri': instance.uri.toString(),
|
'uri': instance.uri.toString(),
|
||||||
'imageIndex': instance.imageIndex,
|
'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 {
|
class ImageModel {
|
||||||
const ImageModel({
|
const ImageModel({
|
||||||
required this.uri,
|
required this.uri,
|
||||||
required this.imageIndex,
|
required this.imageIndex,
|
||||||
required this.imageName,
|
required this.imageName,
|
||||||
|
required this.isFavourite,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// An image's target [Uri].
|
/// An image's target [Uri].
|
||||||
|
@ -21,8 +19,31 @@ class ImageModel {
|
||||||
/// Given name of the image.
|
/// Given name of the image.
|
||||||
final String imageName;
|
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.
|
factory ImageModel.fromDto({
|
||||||
Map<String, dynamic> toJson() => _$ImageModelToJson(this);
|
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: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/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/features/core/services/logging_service.dart';
|
||||||
import 'package:mc_gallery/locator.dart';
|
import 'package:mc_gallery/locator.dart';
|
||||||
|
|
||||||
class ImageCacheManagerService with LoggingService {
|
class ImageCacheManagerService with LoggingService {
|
||||||
ImageCacheManagerService({
|
ImageCacheManagerService(
|
||||||
required AppLifecycleService appLifecycleService,
|
{required AppLifecycleService appLifecycleService,
|
||||||
}) : _appLifecycleService = appLifecycleService {
|
required LocalStorageService localStorageService})
|
||||||
|
: _appLifecycleService = appLifecycleService,
|
||||||
|
_localStorageService = localStorageService {
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final AppLifecycleService _appLifecycleService;
|
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 {
|
Future<void> _init() async {
|
||||||
_appLifecycleService.addListener(
|
_appLifecycleService.addListener(
|
||||||
|
@ -27,7 +32,8 @@ class ImageCacheManagerService with LoggingService {
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
info('Discarding cached images');
|
info('Discarding cached images');
|
||||||
await DefaultCacheManager().emptyCache();
|
await _cacheManager.emptyCache();
|
||||||
|
_localStorageService.resetFavourites();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import 'dart:async';
|
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/constants/const_sorters.dart';
|
||||||
import '/features/core/data/extensions/iterable_extensions.dart';
|
import '/features/core/data/extensions/iterable_extensions.dart';
|
||||||
import '/features/core/data/extensions/map_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/services/logging_service.dart';
|
||||||
import '/features/core/utils/mutex.dart';
|
import '/features/core/utils/mutex.dart';
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
|
@ -19,16 +23,19 @@ import '../data/models/image_model.dart';
|
||||||
class ImagesService {
|
class ImagesService {
|
||||||
ImagesService({
|
ImagesService({
|
||||||
required ImagesApi imagesApi,
|
required ImagesApi imagesApi,
|
||||||
|
required LocalStorageService localStorageService,
|
||||||
required LoggingService loggingService,
|
required LoggingService loggingService,
|
||||||
}) : _imagesApi = imagesApi,
|
}) : _imagesApi = imagesApi,
|
||||||
|
_localStorageService = localStorageService,
|
||||||
_loggingService = loggingService {
|
_loggingService = loggingService {
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
final ImagesApi _imagesApi;
|
final ImagesApi _imagesApi;
|
||||||
|
final LocalStorageService _localStorageService;
|
||||||
final LoggingService _loggingService;
|
final LoggingService _loggingService;
|
||||||
|
|
||||||
late final Map<String, ImageModel> _imageModels;
|
late final LinkedHashMap<String, ImageModel> _imageModels;
|
||||||
Iterable<ImageModel> get imageModels => _imageModels.values.deepCopy;
|
Iterable<ImageModel> get imageModels => _imageModels.values.deepCopy;
|
||||||
|
|
||||||
final Mutex _searchMutex = Mutex();
|
final Mutex _searchMutex = Mutex();
|
||||||
|
@ -41,11 +48,37 @@ class ImagesService {
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_loggingService.info('Fetching and creating image models...');
|
_loggingService.info('Fetching and creating image models...');
|
||||||
_imageModels = {
|
|
||||||
for (final imageModel in (await _imagesApi.fetchImageUri(token: ''))
|
final fetchedImageModelDtos = await _imagesApi.fetchImageUri(token: '');
|
||||||
.map((final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized)))
|
final favouritesStatuses = _localStorageService.storedFavouritesStates;
|
||||||
imageModel.imageName: imageModel
|
|
||||||
};
|
// 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
|
_imageModels.isNotEmpty
|
||||||
? _loggingService.good("Created ${_imageModels.length} images' models")
|
? _loggingService.good("Created ${_imageModels.length} images' models")
|
||||||
|
@ -89,14 +122,20 @@ class ImagesService {
|
||||||
..sort((final a, final b) =>
|
..sort((final a, final b) =>
|
||||||
ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b))
|
ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b))
|
||||||
..reversed;
|
..reversed;
|
||||||
|
|
||||||
return _imageModels.valuesByKeys(keys: rankedKeys).toList(growable: false);
|
return _imageModels.valuesByKeys(keys: rankedKeys).toList(growable: false);
|
||||||
|
|
||||||
case SearchOption.web:
|
case SearchOption.web:
|
||||||
return (await _imagesApi.searchImages(
|
return (await _imagesApi.searchImages(
|
||||||
searchStr: imageNamePart,
|
searchStr: imageNamePart,
|
||||||
token: '',
|
token: '',
|
||||||
))
|
))
|
||||||
.map(
|
.map(
|
||||||
(final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized))
|
(final imageModelDto) => ImageModel.fromDto(
|
||||||
|
imageModelDto: imageModelDto,
|
||||||
|
isFavourite: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
} finally {
|
} 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();
|
static ImagesService get locate => Locator.locate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,30 +15,96 @@ class _DownloadedGalleryView extends StatelessWidget {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
// 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: galleryViewModel.isViewingFavouriteListenable,
|
||||||
spacing: 8,
|
builder: (context, final isViewingFavourites, _) => !isViewingFavourites
|
||||||
alignment: WrapAlignment.center,
|
? CustomWrap(children: [
|
||||||
runAlignment: WrapAlignment.center,
|
for (final imageModel in galleryViewModel.imageModels)
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
_StarrableImage(
|
||||||
children: [
|
key: ValueKey(imageModel.imageIndex),
|
||||||
for (final imageModel in galleryViewModel.imageModels)
|
imageModel: imageModel,
|
||||||
GestureDetector(
|
galleryViewModel: galleryViewModel,
|
||||||
onTap: () => galleryViewModel.pushImageCarouselView(
|
),
|
||||||
context,
|
])
|
||||||
imageModel: imageModel,
|
: CustomWrap(
|
||||||
|
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,7 @@
|
||||||
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/data/constants/const_media.dart';
|
||||||
|
import 'package:mc_gallery/features/home/widgets/custom_wrap.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';
|
||||||
|
@ -81,7 +83,21 @@ class GalleryView extends StatelessWidget {
|
||||||
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
||||||
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
||||||
child: !isSearching
|
child: !isSearching
|
||||||
? _DownloadedGalleryView(galleryViewModel: model)
|
? Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: model.isViewingFavouriteListenable,
|
||||||
|
builder:
|
||||||
|
(context, final isViewingFavourites, child) =>
|
||||||
|
Switch(
|
||||||
|
value: isViewingFavourites,
|
||||||
|
onChanged: model.onFavouriteViewChange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_DownloadedGalleryView(galleryViewModel: model),
|
||||||
|
],
|
||||||
|
)
|
||||||
: _SearchGalleryView(galleryViewModel: model),
|
: _SearchGalleryView(galleryViewModel: model),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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';
|
||||||
|
import 'package:mc_gallery/features/core/data/extensions/value_notifier_extensions.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';
|
||||||
|
@ -27,6 +28,7 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
|
|
||||||
final ImagesService _imagesService;
|
final ImagesService _imagesService;
|
||||||
final NavigationService _navigationService;
|
final NavigationService _navigationService;
|
||||||
|
//todo(mehul): Use to implement pull-to-refresh or an extra widget
|
||||||
final ImageCacheManagerService _imageCacheManagerService;
|
final ImageCacheManagerService _imageCacheManagerService;
|
||||||
final LoggingService _loggingService;
|
final LoggingService _loggingService;
|
||||||
|
|
||||||
|
@ -40,6 +42,9 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
|
final ValueNotifier<List<ImageModel>> _imageSearchResultsNotifier = ValueNotifier([]);
|
||||||
ValueListenable<List<ImageModel>> get imageSearchResultsListenable => _imageSearchResultsNotifier;
|
ValueListenable<List<ImageModel>> get imageSearchResultsListenable => _imageSearchResultsNotifier;
|
||||||
|
|
||||||
|
final ValueNotifier<bool> _isViewingFavouriteNotifier = ValueNotifier(false);
|
||||||
|
ValueListenable<bool> get isViewingFavouriteListenable => _isViewingFavouriteNotifier;
|
||||||
|
|
||||||
@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);
|
||||||
|
@ -80,7 +85,7 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
_loggingService.info('Clearing of results on view mode change');
|
_loggingService.info('Clearing of results on view mode change');
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSearchingNotifier.value = !_isSearchingNotifier.value;
|
_isSearchingNotifier.flipValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> get lastQueryResultDone => _imagesService.lastQueryIsCompleted;
|
Future<void> get lastQueryResultDone => _imagesService.lastQueryIsCompleted;
|
||||||
|
@ -92,9 +97,24 @@ class GalleryViewModel extends BaseViewModel {
|
||||||
_imageSearchResultsNotifier.value = [];
|
_imageSearchResultsNotifier.value = [];
|
||||||
_loggingService.info('Cleared resultsw from view');
|
_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;
|
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
|
||||||
|
|
||||||
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
||||||
|
|
|
@ -29,12 +29,7 @@ class _SearchGalleryView extends StatelessWidget {
|
||||||
builder: (context, final searchOption, child) {
|
builder: (context, final searchOption, child) {
|
||||||
switch (searchOption) {
|
switch (searchOption) {
|
||||||
case SearchOption.local:
|
case SearchOption.local:
|
||||||
return Wrap(
|
return CustomWrap(
|
||||||
runSpacing: 24,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
for (final resultsImageModel in resultsImageModels)
|
for (final resultsImageModel in resultsImageModels)
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
|
@ -48,12 +43,7 @@ class _SearchGalleryView extends StatelessWidget {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case SearchOption.web:
|
case SearchOption.web:
|
||||||
return Wrap(
|
return CustomWrap(
|
||||||
runSpacing: 24,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
for (final imageResult in resultsImageModels)
|
for (final imageResult in resultsImageModels)
|
||||||
Image.network(
|
Image.network(
|
||||||
|
|
|
@ -3,7 +3,6 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.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_colors.dart';
|
||||||
import '/features/core/data/constants/const_text.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/core/widgets/mcg_scaffold.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';
|
import '../../../core/widgets/state/view_model_builder.dart';
|
||||||
|
import '../../data/models/image_model.dart';
|
||||||
|
|
||||||
class ImageCarouselViewArguments {
|
class ImageCarouselViewArguments {
|
||||||
const ImageCarouselViewArguments({required this.imageIndexKey});
|
const ImageCarouselViewArguments({required this.imageIndexKey});
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:flutter_cache_manager/flutter_cache_manager.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';
|
||||||
import '/features/core/services/navigation_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/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/models/image_model.dart';
|
||||||
|
|
||||||
class ImageCarouselViewModel extends BaseViewModel {
|
class ImageCarouselViewModel extends BaseViewModel {
|
||||||
ImageCarouselViewModel({
|
ImageCarouselViewModel({
|
||||||
|
|
22
lib/features/home/widgets/custom_wrap.dart
Normal file
22
lib/features/home/widgets/custom_wrap.dart
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class CustomWrap extends StatelessWidget {
|
||||||
|
const CustomWrap({
|
||||||
|
required this.children,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Wrap(
|
||||||
|
runSpacing: 24,
|
||||||
|
spacing: 8,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||||
import 'features/core/abstracts/router/app_router.dart';
|
import 'features/core/abstracts/router/app_router.dart';
|
||||||
import 'features/core/services/app_lifecycle_service.dart';
|
import 'features/core/services/app_lifecycle_service.dart';
|
||||||
import 'features/core/services/connections_service.dart';
|
import 'features/core/services/connections_service.dart';
|
||||||
|
import 'features/core/services/local_storage_service.dart';
|
||||||
import 'features/core/services/logging_service.dart';
|
import 'features/core/services/logging_service.dart';
|
||||||
import 'features/core/services/navigation_service.dart';
|
import 'features/core/services/navigation_service.dart';
|
||||||
import 'features/core/services/overlay_service.dart';
|
import 'features/core/services/overlay_service.dart';
|
||||||
|
@ -94,14 +95,24 @@ class Locator {
|
||||||
dispose: (final param) async => await param.dispose(),
|
dispose: (final param) async => await param.dispose(),
|
||||||
);
|
);
|
||||||
|
|
||||||
it.registerSingleton<ImagesService>(
|
it.registerSingleton<LocalStorageService>(
|
||||||
ImagesService(imagesApi: UnsplashImagesApi.locate, loggingService: LoggingService.locate),
|
LocalStorageService(),
|
||||||
|
signalsReady: true,
|
||||||
|
);
|
||||||
|
await it.isReady<LocalStorageService>();
|
||||||
|
|
||||||
|
it.registerSingleton<ImagesService>(
|
||||||
|
ImagesService(
|
||||||
|
imagesApi: UnsplashImagesApi.locate,
|
||||||
|
localStorageService: LocalStorageService.locate,
|
||||||
|
loggingService: LoggingService.locate,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
//await it.isReady<ImagesService>();
|
|
||||||
|
|
||||||
it.registerSingleton(
|
it.registerSingleton(
|
||||||
ImageCacheManagerService(
|
ImageCacheManagerService(
|
||||||
appLifecycleService: AppLifecycleService.locate,
|
appLifecycleService: AppLifecycleService.locate,
|
||||||
|
localStorageService: LocalStorageService.locate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
21
pubspec.lock
21
pubspec.lock
|
@ -345,6 +345,27 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
hive:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
|
hive_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: hive_flutter
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
hive_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: hive_generator
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -25,6 +25,10 @@ dependencies:
|
||||||
cached_network_image: ^3.2.3
|
cached_network_image: ^3.2.3
|
||||||
flutter_cache_manager: ^3.3.0
|
flutter_cache_manager: ^3.3.0
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
hive: ^2.2.3
|
||||||
|
hive_flutter: ^1.1.0
|
||||||
|
|
||||||
# Util backend
|
# Util backend
|
||||||
intl_utils: ^2.8.1
|
intl_utils: ^2.8.1
|
||||||
connectivity_plus: ^3.0.2
|
connectivity_plus: ^3.0.2
|
||||||
|
@ -52,8 +56,11 @@ dev_dependencies:
|
||||||
|
|
||||||
# Builders
|
# Builders
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
json_annotation: ^4.7.0
|
|
||||||
json_serializable: ^6.5.4
|
json_serializable: ^6.5.4
|
||||||
|
hive_generator: ^2.0.0
|
||||||
|
|
||||||
|
# Annotations
|
||||||
|
json_annotation: ^4.7.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
Loading…
Reference in a new issue