mc_gallery/lib/features/home/services/images_service.dart

110 lines
4.3 KiB
Dart

import 'dart:async';
import 'package:mc_gallery/features/core/data/extensions/string_extensions.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/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 LoggingService loggingService,
}) : _imagesApi = imagesApi,
_loggingService = loggingService {
_init();
}
final ImagesApi _imagesApi;
final LoggingService _loggingService;
late final Map<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...');
_imageModels = {
for (final imageModel in (await _imagesApi.fetchImageUri(token: ''))
.map((final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized)))
imageModel.imageName: imageModel
};
_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 emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized))
.toList(growable: false);
}
} finally {
unlock();
}
});
}
static ImagesService get locate => Locator.locate();
}