163 lines
6.2 KiB
Dart
163 lines
6.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
|
|
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';
|
|
import '../abstracts/images_api.dart';
|
|
import '../data/enums/search_option.dart';
|
|
import '../data/models/image_model.dart';
|
|
|
|
/// Handles fetching and storing of Images.
|
|
///
|
|
/// Since this is very simple use-case, this is the only interface. For complex (actual CRUD-based) I/O,
|
|
/// an additional Repository layer interface can be used between [ImagesService] and [ImagesApi].
|
|
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 LinkedHashMap<String, ImageModel> _imageModels;
|
|
Iterable<ImageModel> get imageModels => _imageModels.values.deepCopy;
|
|
|
|
final Mutex _searchMutex = Mutex();
|
|
|
|
/// Manual initialization triggering
|
|
///
|
|
/// Since this service not critical to app-start-up [Locator]'s `signalsReady=true` is not used.
|
|
final Completer _initAwaiter = Completer();
|
|
Future get initAwaiter => _initAwaiter.future;
|
|
|
|
Future<void> _init() async {
|
|
_loggingService.info('Fetching and creating image models...');
|
|
|
|
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")
|
|
: _loggingService.warning('No images found');
|
|
|
|
_initAwaiter.complete();
|
|
}
|
|
|
|
int get firstAvailableImageIndex => 0;
|
|
int get lastAvailableImageIndex => _imageModels.length - 1;
|
|
int get numberOfImages => _imageModels.length;
|
|
|
|
ImageModel imageModelAt({required int index}) => _imageModels.values.elementAt(index);
|
|
|
|
Future<void> get lastQueryIsCompleted => _searchMutex.lastOperationCompletionAwaiter;
|
|
|
|
/// Performs searching on images, both locally and by a Web API endpoint.
|
|
///
|
|
/// For now, a simple mechanism is used for handling async calls between (posssible) API fetches ->
|
|
/// just 'pile-up'. A mechanism can be made to 'cancel' a fetch if a newer search request comes in,
|
|
/// but that may be more complicated, and not the point of the assignment I think.
|
|
/// There are lots of optimizations possible for new inputs, for example reducing search frontier
|
|
/// by using set-cover/subsetting optimizations on backspace, and so on, but again, not the point,
|
|
/// I think.
|
|
Future<List<ImageModel>> searchImages({
|
|
required SearchOption searchOption,
|
|
required String imageNamePart,
|
|
bool treatAsInSequence = false,
|
|
}) async {
|
|
return await _searchMutex.lockAndRun(run: (final unlock) async {
|
|
try {
|
|
switch (searchOption) {
|
|
case SearchOption.local:
|
|
final rankedKeys = _imageModels.keys
|
|
// Reduce number of results by atleast occurring
|
|
.where((final imageName) => treatAsInSequence
|
|
? imageName.contains(imageNamePart)
|
|
: imageName.containsAllCharacters(targetChars: imageNamePart))
|
|
.toList(growable: false)
|
|
// Sorting by the highest similarity first
|
|
..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 imageModelDto) => ImageModel.fromDto(
|
|
imageModelDto: imageModelDto,
|
|
isFavourite: false,
|
|
),
|
|
)
|
|
.toList(growable: false);
|
|
}
|
|
} finally {
|
|
unlock();
|
|
}
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|