diff --git a/README.md b/README.md
index 9567448..5b90e68 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,13 @@
# mc_gallery
-A new Flutter project.
+## Dart docs explanation
-## Getting Started
+## Emulation
-This project is a starting point for a Flutter application.
+## Maintaining scope
+It's an 'assignment'
-A few resources to get you started if this is your first Flutter project:
+## Model vs. DTO
-- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
-
-For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+## Extra quirks
+Just because I had those assets lying around
\ No newline at end of file
diff --git a/lib/features/core/data/constants/const_sorters.dart b/lib/features/core/data/constants/const_sorters.dart
new file mode 100644
index 0000000..a528a14
--- /dev/null
+++ b/lib/features/core/data/constants/const_sorters.dart
@@ -0,0 +1,8 @@
+import 'package:string_similarity/string_similarity.dart';
+
+abstract class ConstSorters {
+ /// Uses Dice's Coefficient as a similarity metric, for a 2-way comparison, between a [targetWord]
+ /// and given words.
+ static int stringsSimilarityTarget(String a, String b, {required String targetWord}) =>
+ a.similarityTo(targetWord).compareTo(b.similarityTo(targetWord));
+}
diff --git a/lib/features/core/data/extensions/map_extensions.dart b/lib/features/core/data/extensions/map_extensions.dart
index a185aa1..26f310a 100644
--- a/lib/features/core/data/extensions/map_extensions.dart
+++ b/lib/features/core/data/extensions/map_extensions.dart
@@ -1,3 +1,6 @@
extension MapExtensions on Map {
Map get deepCopy => {...this};
+
+ /// Returns the values of a [Map] at given [keys] indices.
+ Iterable valuesByKeys({required Iterable keys}) => keys.map((final key) => this[key]!);
}
diff --git a/lib/features/home/abstracts/images_api.dart b/lib/features/home/abstracts/images_api.dart
index a0b2475..de9314f 100644
--- a/lib/features/home/abstracts/images_api.dart
+++ b/lib/features/home/abstracts/images_api.dart
@@ -1,15 +1,13 @@
import 'dart:async';
-import '../data/models/image_model.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> fetchImageUri({required String token});
+ FutureOr>> fetchImageUri({required String token});
- FutureOr> searchImages({
+ FutureOr>> searchImages({
required String searchStr,
required String token,
});
diff --git a/lib/features/home/api/unsplash_images_api.dart b/lib/features/home/api/unsplash_images_api.dart
index 8efe727..0ed3974 100644
--- a/lib/features/home/api/unsplash_images_api.dart
+++ b/lib/features/home/api/unsplash_images_api.dart
@@ -14,14 +14,15 @@ class UnsplashImagesApi implements ImagesApi {
final random = Random();
@override
- FutureOr> fetchImageUri({required String token}) async {
+ FutureOr>> fetchImageUri({required String token}) async {
// Dummy fetching delay emulation
await Future.delayed(const Duration(
milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages));
try {
// Create fixed number of images
- return Iterable.generate(ConstValues.numberOfImages).map((final imageIndex) {
+ final dummyImageModels =
+ Iterable.generate(ConstValues.numberOfImages).map((final imageIndex) {
// Drawing from a normal distribution
final imageSide =
random.nextIntInRange(min: ConstValues.minImageSize, max: ConstValues.maxImageSize);
@@ -36,6 +37,9 @@ class UnsplashImagesApi implements ImagesApi {
imageName: Strings.current.imageNameFetch(imageIndex + 1, imageSide),
);
});
+
+ // Emulating serialization
+ return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
} on Exception catch (ex, stackTrace) {
_loggingService.handleException(ex, stackTrace);
return const Iterable.empty();
@@ -43,7 +47,7 @@ class UnsplashImagesApi implements ImagesApi {
}
@override
- FutureOr> searchImages({
+ FutureOr>> searchImages({
required String searchStr,
required String token,
}) async {
@@ -55,7 +59,7 @@ class UnsplashImagesApi implements ImagesApi {
try {
// Create (randomly-bounded) dummy number of images
- return Iterable.generate(numberOfResults).map((final imageIndex) {
+ final dummyImageModels = Iterable.generate(numberOfResults).map((final imageIndex) {
// Drawing from a normal distribution
final imageSide =
random.nextIntInRange(min: ConstValues.minImageSize, max: ConstValues.maxImageSize);
@@ -68,7 +72,10 @@ class UnsplashImagesApi implements ImagesApi {
// Custom dummy name for the image
imageName: Strings.current.imageNameSearch(searchStr, imageIndex + 1),
);
- }).toList(growable: false);
+ });
+
+ // Emulating serialization
+ return dummyImageModels.map((final dummyModel) => dummyModel.toJson());
} on Exception catch (ex, stackTrace) {
_loggingService.handleException(ex, stackTrace);
return List.empty();
diff --git a/lib/features/home/data/models/image_model.dart b/lib/features/home/data/models/image_model.dart
index 959fb3e..4765552 100644
--- a/lib/features/home/data/models/image_model.dart
+++ b/lib/features/home/data/models/image_model.dart
@@ -1,3 +1,8 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'image_model.g.dart';
+
+@JsonSerializable()
class ImageModel {
const ImageModel({
required this.uri,
@@ -15,4 +20,9 @@ class ImageModel {
/// Given name of the image.
final String imageName;
+
+ factory ImageModel.fromJson(Map json) => _$ImageModelFromJson(json);
+
+ /// Connect the generated [_$PersonToJson] function to the `toJson` method.
+ Map toJson() => _$ImageModelToJson(this);
}
diff --git a/lib/features/home/data/models/image_model.g.dart b/lib/features/home/data/models/image_model.g.dart
new file mode 100644
index 0000000..fd16a20
--- /dev/null
+++ b/lib/features/home/data/models/image_model.g.dart
@@ -0,0 +1,20 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'image_model.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ImageModel _$ImageModelFromJson(Map json) => ImageModel(
+ uri: Uri.parse(json['uri'] as String),
+ imageIndex: json['imageIndex'] as int,
+ imageName: json['imageName'] as String,
+ );
+
+Map _$ImageModelToJson(ImageModel instance) =>
+ {
+ 'uri': instance.uri.toString(),
+ 'imageIndex': instance.imageIndex,
+ 'imageName': instance.imageName,
+ };
diff --git a/lib/features/home/services/images_service.dart b/lib/features/home/services/images_service.dart
index 0bf756c..9a1fda7 100644
--- a/lib/features/home/services/images_service.dart
+++ b/lib/features/home/services/images_service.dart
@@ -1,12 +1,14 @@
import 'dart:async';
+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 '/features/home/data/models/image_model.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.
///
@@ -24,8 +26,9 @@ class ImagesService {
final ImagesApi _imagesApi;
final LoggingService _loggingService;
- late final Iterable _imageModels;
- Iterable get imageModels => _imageModels.deepCopy;
+ late final Map _imageModels;
+ Iterable get imageModels => _imageModels.values.deepCopy;
+
final Mutex _searchMutex = Mutex();
/// Manual initialization triggering
@@ -36,7 +39,11 @@ class ImagesService {
Future _init() async {
_loggingService.info('Fetching and creating image models...');
- _imageModels = await _imagesApi.fetchImageUri(token: '');
+ _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")
@@ -49,24 +56,42 @@ class ImagesService {
int get lastAvailableImageIndex => _imageModels.length - 1;
int get numberOfImages => _imageModels.length;
- ImageModel imageModelAt({required int index}) => _imageModels.elementAt(index);
+ ImageModel imageModelAt({required int index}) => _imageModels.values.elementAt(index);
Future get lastQueryIsCompleted => _searchMutex.lastOperationCompletionAwaiter;
- Future> searchImages({
- required SearchOption searchOption,
- required String imageNamePart,
- }) async {
+ /// 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> 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:
- return [];
+ final rankedKeys = _imageModels.keys
+ //todo(mehul): Implement atleast-matching-all-parts
+ .where(
+ (final imageName) => imageName.contains(treatAsInSequence ? imageNamePart : ''))
+ .toList(growable: false)
+ ..sort((final a, final b) =>
+ ConstSorters.stringsSimilarityTarget(targetWord: imageNamePart, a, b));
+ return _imageModels.valuesByKeys(keys: rankedKeys).toList(growable: false);
case SearchOption.web:
- return await _imagesApi.searchImages(
+ return (await _imagesApi.searchImages(
searchStr: imageNamePart,
token: '',
- );
+ ))
+ .map(
+ (final emulatedModelSerialized) => ImageModel.fromJson(emulatedModelSerialized))
+ .toList(growable: false);
}
} finally {
unlock();
diff --git a/lib/features/home/views/gallery/gallery_view_model.dart b/lib/features/home/views/gallery/gallery_view_model.dart
index f6e274b..ac15c37 100644
--- a/lib/features/home/views/gallery/gallery_view_model.dart
+++ b/lib/features/home/views/gallery/gallery_view_model.dart
@@ -7,12 +7,12 @@ 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/image_cache_manager_service.dart';
-import '/features/home/services/images_service.dart';
-import '/features/home/views/image_carousel/image_carousel_view.dart';
import '/locator.dart';
import '../../data/enums/search_option.dart';
+import '../../data/models/image_model.dart';
+import '../../services/image_cache_manager_service.dart';
+import '../../services/images_service.dart';
+import '../image_carousel/image_carousel_view.dart';
class GalleryViewModel extends BaseViewModel {
GalleryViewModel({
@@ -54,17 +54,22 @@ class GalleryViewModel extends BaseViewModel {
// If empty-string (from backspacing) -> reset state.
if (searchTerm.isEmpty) {
_imageSearchResultsNotifier.value = [];
+ _loggingService.info('Clearing results on search string removal');
return;
}
// Detached call to prevent UI blocking
- unawaited(_imagesService
- .searchImages(
- imageNamePart: searchTerm,
- searchOption: searchOptionListenable.value,
- )
- .then(
- (final fetchedImageModels) => _imageSearchResultsNotifier.value = fetchedImageModels));
+ unawaited(
+ _imagesService
+ .searchImages(
+ imageNamePart: searchTerm,
+ searchOption: searchOptionListenable.value,
+ // todo(mehul): When implemented, remove this
+ treatAsInSequence: true,
+ )
+ .then(
+ (final fetchedImageModels) => _imageSearchResultsNotifier.value = fetchedImageModels),
+ );
// Force-update to trigger listening to `lastQueryResultDone()`.
_imageSearchResultsNotifier.notifyListeners();
@@ -72,14 +77,20 @@ class GalleryViewModel extends BaseViewModel {
void searchPressed() {
// If transitioning from 'Searching', clear previous results immediately
- if (_isSearchingNotifier.value) _imageSearchResultsNotifier.value = [];
+ if (_isSearchingNotifier.value) {
+ _imageSearchResultsNotifier.value = [];
+ _loggingService.info('Clearing results on view mode change');
+ }
_isSearchingNotifier.value = !_isSearchingNotifier.value;
}
Future get lastQueryResultDone => _imagesService.lastQueryIsCompleted;
- void onSearchOptionChanged(SearchOption? option) => _searchOptionNotifier.value = option!;
+ void onSearchOptionChanged(SearchOption? option) {
+ _searchOptionNotifier.value = option!;
+ _loggingService.info('Switching over to $option search');
+ }
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
diff --git a/pubspec.lock b/pubspec.lock
index ca2f9cb..5a3a54c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -57,6 +57,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
+ build:
+ dependency: transitive
+ description:
+ name: build
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.1"
+ build_config:
+ dependency: transitive
+ description:
+ name: build_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1"
+ build_daemon:
+ dependency: transitive
+ description:
+ name: build_daemon
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.0"
+ build_resolvers:
+ dependency: transitive
+ description:
+ name: build_resolvers
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ build_runner:
+ dependency: "direct dev"
+ description:
+ name: build_runner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.3.3"
+ build_runner_core:
+ dependency: transitive
+ description:
+ name: build_runner_core
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "7.2.7"
+ built_collection:
+ dependency: transitive
+ description:
+ name: built_collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.1.1"
+ built_value:
+ dependency: transitive
+ description:
+ name: built_value
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "8.4.2"
cached_network_image:
dependency: "direct main"
description:
@@ -92,6 +148,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
clock:
dependency: transitive
description:
@@ -99,6 +162,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
+ code_builder:
+ dependency: transitive
+ description:
+ name: code_builder
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.4.0"
collection:
dependency: transitive
description:
@@ -183,6 +253,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.4"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
flutter:
dependency: "direct main"
description: flutter
@@ -233,6 +310,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.2.0"
get_it:
dependency: "direct main"
description:
@@ -254,6 +338,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
+ graphs:
+ dependency: transitive
+ description:
+ name: graphs
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
http:
dependency: transitive
description:
@@ -261,6 +352,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.5"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.2.1"
http_parser:
dependency: transitive
description:
@@ -289,6 +387,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.1"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
js:
dependency: transitive
description:
@@ -296,6 +401,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
+ json_annotation:
+ dependency: "direct dev"
+ description:
+ name: json_annotation
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.7.0"
+ json_serializable:
+ dependency: "direct dev"
+ description:
+ name: json_serializable
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.5.4"
lints:
dependency: transitive
description:
@@ -338,6 +457,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
nested:
dependency: transitive
description:
@@ -471,6 +597,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.2"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.5.1"
process:
dependency: transitive
description:
@@ -492,6 +625,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.1"
rxdart:
dependency: transitive
description:
@@ -499,11 +639,39 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.7"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.0"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
+ source_gen:
+ dependency: transitive
+ description:
+ name: source_gen
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.6"
+ source_helper:
+ dependency: transitive
+ description:
+ name: source_helper
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.3"
source_span:
dependency: transitive
description:
@@ -539,6 +707,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
string_scanner:
dependency: transitive
description:
@@ -546,6 +721,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
+ string_similarity:
+ dependency: "direct main"
+ description:
+ name: string_similarity
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
synchronized:
dependency: transitive
description:
@@ -588,6 +770,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.12"
+ timing:
+ dependency: transitive
+ description:
+ name: timing
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
typed_data:
dependency: transitive
description:
@@ -616,6 +805,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
win32:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 1a242bf..5bc5132 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -29,6 +29,7 @@ dependencies:
intl_utils: ^2.8.1
connectivity_plus: ^3.0.2
internet_connection_checker: ^1.0.0+1
+ string_similarity: ^2.0.0
# Util frontend
flutter_markdown: ^0.6.13
@@ -49,6 +50,11 @@ dev_dependencies:
flutter_lints: ^2.0.1
+ # Builders
+ build_runner: ^2.3.3
+ json_annotation: ^4.7.0
+ json_serializable: ^6.5.4
+
flutter:
uses-material-design: true