refactoring
security update
This commit is contained in:
parent
1747ab0245
commit
78fe9e7b09
39 changed files with 289 additions and 132 deletions
|
@ -7,10 +7,15 @@ import '../data/dtos/image_model_dto.dart';
|
|||
/// 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<ImageModelDTO>> fetchImageUri({required String token});
|
||||
ImagesApi({required String token}) : _token = token;
|
||||
|
||||
/// Access token provided to be used with API calls
|
||||
final String _token;
|
||||
|
||||
/// Returns images fetched through an API as [ImageModelDTO]s.
|
||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri();
|
||||
|
||||
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||
required String searchStr,
|
||||
required String token,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,12 +9,14 @@ import '/locator.dart';
|
|||
import '../abstracts/images_api.dart';
|
||||
import '../data/dtos/image_model_dto.dart';
|
||||
|
||||
class UnsplashImagesApi implements ImagesApi {
|
||||
class UnsplashImagesApi extends ImagesApi {
|
||||
final LoggingService _loggingService = LoggingService.locate;
|
||||
final random = Random();
|
||||
|
||||
UnsplashImagesApi({required super.token});
|
||||
|
||||
@override
|
||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token}) async {
|
||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri() async {
|
||||
// Dummy fetching delay emulation
|
||||
await Future.delayed(const Duration(
|
||||
milliseconds: ConstValues.defaultEmulatedLatencyMillis * ConstValues.numberOfImages));
|
||||
|
@ -54,7 +56,6 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
@override
|
||||
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||
required String searchStr,
|
||||
required String token,
|
||||
}) async {
|
||||
final numberOfResults = random.nextIntInRange(min: 0, max: ConstValues.numberOfImages);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import '/l10n/generated/l10n.dart';
|
||||
|
||||
/// Represents an option for specifying a search strategy, for an [ImageModel]
|
||||
enum SearchOption {
|
||||
local,
|
||||
web;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import '../dtos/image_model_dto.dart';
|
||||
|
||||
/// Represents an Image, that would be displayed in the gallery.
|
||||
class ImageModel {
|
||||
const ImageModel({
|
||||
required this.uri,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 {
|
||||
import '/features/core/services/app_lifecycle_service.dart';
|
||||
import '/features/core/services/local_storage_service.dart';
|
||||
import '/features/core/services/logging_service.dart';
|
||||
import '/locator.dart';
|
||||
|
||||
/// Handles maintaining the caching of downloaded images
|
||||
class ImageCacheManagerService {
|
||||
ImageCacheManagerService(
|
||||
{required AppLifecycleService appLifecycleService,
|
||||
required LocalStorageService localStorageService})
|
||||
|
@ -17,6 +19,7 @@ class ImageCacheManagerService with LoggingService {
|
|||
|
||||
final AppLifecycleService _appLifecycleService;
|
||||
final LocalStorageService _localStorageService;
|
||||
final LoggingService _loggingService = LoggingService.locate;
|
||||
final _cacheManager = DefaultCacheManager();
|
||||
|
||||
Future<void> emptyCache() async => await _cacheManager.emptyCache();
|
||||
|
@ -31,7 +34,7 @@ class ImageCacheManagerService with LoggingService {
|
|||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
info('Discarding cached images');
|
||||
_loggingService.info('Discarding cached images');
|
||||
await _cacheManager.emptyCache();
|
||||
_localStorageService.resetFavourites();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ 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';
|
||||
|
@ -11,6 +10,7 @@ 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 '/features/home/data/dtos/image_model_dto.dart';
|
||||
import '/locator.dart';
|
||||
import '../abstracts/images_api.dart';
|
||||
import '../data/enums/search_option.dart';
|
||||
|
@ -49,7 +49,7 @@ class ImagesService {
|
|||
Future<void> _init() async {
|
||||
_loggingService.info('Fetching and creating image models...');
|
||||
|
||||
final fetchedImageModelDtos = await _imagesApi.fetchImageUri(token: '');
|
||||
final fetchedImageModelDtos = await _imagesApi.fetchImageUri();
|
||||
final favouritesStatuses = _localStorageService.storedFavouritesStates;
|
||||
|
||||
// Prefill from stored values
|
||||
|
@ -128,7 +128,6 @@ class ImagesService {
|
|||
case SearchOption.web:
|
||||
return (await _imagesApi.searchImages(
|
||||
searchStr: imageNamePart,
|
||||
token: '',
|
||||
))
|
||||
.map(
|
||||
(final imageModelDto) => ImageModel.fromDto(
|
||||
|
|
|
@ -18,12 +18,7 @@ class _DownloadedGalleryView extends StatelessWidget {
|
|||
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,
|
||||
? CustomWrap(
|
||||
children: [
|
||||
for (final imageModel in galleryViewModel.imageModels)
|
||||
_StarrableImage(
|
||||
|
@ -33,12 +28,7 @@ class _DownloadedGalleryView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
)
|
||||
: Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
: CustomWrap(
|
||||
children: [
|
||||
for (final favouriteImageModel in galleryViewModel.favouriteImageModels)
|
||||
_StarrableImage(
|
||||
|
@ -88,11 +78,14 @@ class _StarrableImageState extends State<_StarrableImage> {
|
|||
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),
|
||||
child: Hero(
|
||||
tag: widget.imageModel.imageIndex,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget.imageModel.uri.toString(),
|
||||
cacheKey: widget.imageModel.imageIndex.toString(),
|
||||
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
|
||||
value: widget.galleryViewModel.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
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';
|
||||
import '/features/core/data/constants/const_media.dart';
|
||||
import '/features/core/widgets/gap.dart';
|
||||
import '/features/core/widgets/mcg_scaffold.dart';
|
||||
import '/features/core/widgets/state/multi_value_listenable_builder.dart';
|
||||
import '/features/core/widgets/state/view_model_builder.dart';
|
||||
import '/features/home/widgets/custom_wrap.dart';
|
||||
import '../../data/enums/search_option.dart';
|
||||
import '../../data/models/image_model.dart';
|
||||
import 'gallery_view_model.dart';
|
||||
|
@ -81,21 +82,38 @@ class GalleryView extends StatelessWidget {
|
|||
valueListenable: model.isSearchingListenable,
|
||||
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
||||
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
||||
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),
|
||||
],
|
||||
),
|
||||
child: !isSearching
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: model.isViewingFavouriteListenable,
|
||||
builder:
|
||||
(context, final isViewingFavourites, child) =>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ConstMedia.buildIcon(
|
||||
ConstMedia.favStarOutline,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
Switch(
|
||||
value: isViewingFavourites,
|
||||
onChanged: model.onFavouriteViewChange,
|
||||
),
|
||||
ConstMedia.buildIcon(
|
||||
ConstMedia.favStarFilled,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_DownloadedGalleryView(galleryViewModel: model),
|
||||
],
|
||||
)
|
||||
: _SearchGalleryView(galleryViewModel: model),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,10 +3,9 @@ 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';
|
||||
import '/features/core/data/extensions/value_notifier_extensions.dart';
|
||||
import '/features/core/services/navigation_service.dart';
|
||||
import '/locator.dart';
|
||||
import '../../data/enums/search_option.dart';
|
||||
|
@ -20,17 +19,14 @@ class GalleryViewModel extends BaseViewModel {
|
|||
required ImagesService imagesService,
|
||||
required NavigationService navigationService,
|
||||
required ImageCacheManagerService imageCacheManagerService,
|
||||
required LoggingService loggingService,
|
||||
}) : _imagesService = imagesService,
|
||||
_navigationService = navigationService,
|
||||
_imageCacheManagerService = imageCacheManagerService,
|
||||
_loggingService = loggingService;
|
||||
_imageCacheManagerService = imageCacheManagerService;
|
||||
|
||||
final ImagesService _imagesService;
|
||||
final NavigationService _navigationService;
|
||||
//todo(mehul): Use to implement pull-to-refresh or an extra widget
|
||||
final ImageCacheManagerService _imageCacheManagerService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true);
|
||||
ValueListenable<bool> get isDisplayingPressingPrompt => _isDisplayingPressingPrompt;
|
||||
|
@ -59,7 +55,7 @@ class GalleryViewModel extends BaseViewModel {
|
|||
// If empty-string (from backspacing) -> reset state.
|
||||
if (searchTerm.isEmpty) {
|
||||
_imageSearchResultsNotifier.value = [];
|
||||
_loggingService.info('Clearing results on search string removal');
|
||||
log.info('Clearing results on search string removal');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -82,7 +78,7 @@ class GalleryViewModel extends BaseViewModel {
|
|||
// If transitioning from 'Searching', clear previous results immediately
|
||||
if (_isSearchingNotifier.value) {
|
||||
_imageSearchResultsNotifier.value = [];
|
||||
_loggingService.info('Clearing of results on view mode change');
|
||||
log.info('Clearing of results on view mode change');
|
||||
}
|
||||
|
||||
_isSearchingNotifier.flipValue();
|
||||
|
@ -92,10 +88,10 @@ class GalleryViewModel extends BaseViewModel {
|
|||
|
||||
void onSearchOptionChanged(SearchOption? option) {
|
||||
_searchOptionNotifier.value = option!;
|
||||
_loggingService.info('Switched over to $option search');
|
||||
log.info('Switched over to $option search');
|
||||
|
||||
_imageSearchResultsNotifier.value = [];
|
||||
_loggingService.info('Cleared resultsw from view');
|
||||
log.info('Cleared resultsw from view');
|
||||
|
||||
//todo(mehul): Either redo search or force user to type in new (trigger) by clearing field
|
||||
}
|
||||
|
|
|
@ -29,12 +29,7 @@ class _SearchGalleryView extends StatelessWidget {
|
|||
builder: (context, final searchOption, child) {
|
||||
switch (searchOption) {
|
||||
case SearchOption.local:
|
||||
return Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
return CustomWrap(
|
||||
children: [
|
||||
for (final resultsImageModel in resultsImageModels)
|
||||
CachedNetworkImage(
|
||||
|
@ -48,12 +43,7 @@ class _SearchGalleryView extends StatelessWidget {
|
|||
],
|
||||
);
|
||||
case SearchOption.web:
|
||||
return Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
return CustomWrap(
|
||||
children: [
|
||||
for (final imageResult in resultsImageModels)
|
||||
Image.network(
|
||||
|
|
|
@ -62,13 +62,16 @@ class ImageCarouselView extends StatelessWidget {
|
|||
children: [
|
||||
ValueListenableBuilder<ImageModel>(
|
||||
valueListenable: model.currentImageModelListenable,
|
||||
builder: (context, _, __) => CachedNetworkImage(
|
||||
imageUrl: model.currentImageUrl,
|
||||
cacheKey: model.currentImageKey,
|
||||
fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (_, __, final progress) =>
|
||||
CircularProgressIndicator(
|
||||
value: model.downloadProgressValue(progress: progress),
|
||||
builder: (context, _, __) => Hero(
|
||||
tag: model.currentImageIndex,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: model.currentImageUrl,
|
||||
cacheKey: model.currentImageKey,
|
||||
fit: BoxFit.contain,
|
||||
progressIndicatorBuilder: (_, __, final progress) =>
|
||||
CircularProgressIndicator(
|
||||
value: model.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,27 +1,19 @@
|
|||
import 'package:carousel_slider/carousel_controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.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/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({
|
||||
required ImagesService imagesService,
|
||||
required NavigationService navigationService,
|
||||
required LoggingService loggingService,
|
||||
}) : _imagesService = imagesService,
|
||||
_navigationService = navigationService,
|
||||
_loggingService = loggingService;
|
||||
}) : _imagesService = imagesService;
|
||||
|
||||
final ImagesService _imagesService;
|
||||
final NavigationService _navigationService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
late final ValueNotifier<ImageModel> _currentImageModelNotifier;
|
||||
ValueListenable<ImageModel> get currentImageModelListenable => _currentImageModelNotifier;
|
||||
|
@ -31,8 +23,8 @@ class ImageCarouselViewModel extends BaseViewModel {
|
|||
@override
|
||||
Future<void> initialise(bool Function() mounted, [arguments]) async {
|
||||
_currentImageModelNotifier = ValueNotifier(_imagesService.imageModels
|
||||
.elementAt((arguments! as ImageCarouselViewArguments).imageIndexKey));
|
||||
_loggingService.info('Initialized with image: ${_currentImageModelNotifier.value.imageIndex}');
|
||||
.elementAt((arguments as ImageCarouselViewArguments).imageIndexKey));
|
||||
log.info('Initialized with image: ${_currentImageModelNotifier.value.imageIndex}');
|
||||
|
||||
super.initialise(mounted, arguments);
|
||||
}
|
||||
|
@ -44,7 +36,7 @@ class ImageCarouselViewModel extends BaseViewModel {
|
|||
|
||||
void swipedTo({required int newIndex}) {
|
||||
_currentImageModelNotifier.value = _imagesService.imageModelAt(index: newIndex);
|
||||
_loggingService.info('Swiped to image: ${_currentImageModelNotifier.value.imageIndex}');
|
||||
log.info('Swiped to image: ${_currentImageModelNotifier.value.imageIndex}');
|
||||
}
|
||||
|
||||
void onPreviousPressed() => carouselController.previousPage();
|
||||
|
|
30
lib/features/home/widgets/custom_wrap.dart
Normal file
30
lib/features/home/widgets/custom_wrap.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '/features/core/data/constants/const_colors.dart';
|
||||
|
||||
class CustomWrap extends StatelessWidget {
|
||||
const CustomWrap({
|
||||
required this.children,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return children.isNotEmpty
|
||||
? Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: children,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.image,
|
||||
size: 80,
|
||||
color: ConstColours.red,
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue