refactoring
This commit is contained in:
parent
68d7f70ded
commit
841a494503
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'features/core/abstracts/app_setup.dart';
|
import 'features/core/abstracts/app_setup.dart';
|
||||||
import 'features/core/abstracts/router/app_router.dart';
|
import 'features/core/abstracts/router/app_router.dart';
|
||||||
import 'features/core/data/constants/const_colors.dart';
|
import 'features/core/data/constants/const_colors.dart';
|
||||||
|
import 'l10n/generated/l10n.dart';
|
||||||
|
|
||||||
class McgApp extends StatelessWidget {
|
class McgApp extends StatelessWidget {
|
||||||
const McgApp({super.key});
|
const McgApp({super.key});
|
||||||
|
@ -11,7 +12,7 @@ class McgApp extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appRouter = McgRouter.locate.router;
|
final appRouter = McgRouter.locate.router;
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: 'MC Gallery App',
|
title: Strings.current.appTitle,
|
||||||
theme: ConstThemes.materialLightTheme,
|
theme: ConstThemes.materialLightTheme,
|
||||||
darkTheme: ConstThemes.materialDarkTheme,
|
darkTheme: ConstThemes.materialDarkTheme,
|
||||||
supportedLocales: AppSetup.supportedLocales,
|
supportedLocales: AppSetup.supportedLocales,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -49,5 +50,11 @@ abstract class AppSetup {
|
||||||
static void Function(Object error, StackTrace stackTrace) get onUncaughtException => (
|
static void Function(Object error, StackTrace stackTrace) get onUncaughtException => (
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
) {};
|
) {
|
||||||
|
log(
|
||||||
|
'Error occurred during app startup',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:mc_gallery/features/home/views/gallery/gallery_view.dart';
|
|
||||||
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart';
|
|
||||||
|
|
||||||
|
import '/features/home/views/gallery/gallery_view.dart';
|
||||||
|
import '/features/home/views/image_carousel/image_carousel_view.dart';
|
||||||
import '../../views/error_page_view.dart';
|
import '../../views/error_page_view.dart';
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ class McgRouter {
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
child: ErrorPageView(error: state.error),
|
child: ErrorPageView(error: state.error),
|
||||||
),
|
),
|
||||||
//todo(mehul): Add Redirect
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.home.routePath,
|
path: Routes.home.routePath,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
enum Routes {
|
enum Routes {
|
||||||
home(RoutesInfo(
|
home(RoutesInfo(
|
||||||
routePath: '/gallery',
|
routePath: '/',
|
||||||
routeName: 'Home',
|
routeName: 'Home',
|
||||||
)),
|
)),
|
||||||
imageCarousel(RoutesInfo(
|
imageCarousel(RoutesInfo(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
abstract class ConstColours {
|
abstract class ConstColours {
|
||||||
/// Smoke Gray => a neutral grey with neutral warmth/cold
|
/// Smoke Gray => a neutral grey with neutral warmth/cold
|
||||||
static const galleryBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
|
static const galleryBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
|
||||||
|
static const red = Colors.red;
|
||||||
|
|
||||||
static const white = Colors.white;
|
static const white = Colors.white;
|
||||||
static const black = Colors.black;
|
static const black = Colors.black;
|
||||||
|
|
|
@ -3,7 +3,7 @@ abstract class ConstValues {
|
||||||
static const String backendHost = 'source.unsplash.com';
|
static const String backendHost = 'source.unsplash.com';
|
||||||
static const List<String> backendUrlPathSegments = ['user', 'c_v_r'];
|
static const List<String> backendUrlPathSegments = ['user', 'c_v_r'];
|
||||||
|
|
||||||
static const int numberOfImages = 20;
|
static const int numberOfImages = 25;
|
||||||
static const int minImageSize = 50;
|
static const int minImageSize = 50;
|
||||||
static const int maxImageSize = 100;
|
static const int maxImageSize = 100;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ extension MapExtensions<A, B> on Map<A, B> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LinkedHashMapExtensions<A, B> on LinkedHashMap<A, B> {
|
extension LinkedHashMapExtensions<A, B> on LinkedHashMap<A, B> {
|
||||||
/// Updated the value at [valueIndex] to [newValue], in addition to preserving the order.
|
/// Updates the value at [valueIndex] to [newValue], in addition to preserving the order.
|
||||||
void updateValueAt({
|
void updateValueAt({
|
||||||
required int valueIndex,
|
required int valueIndex,
|
||||||
required B newValue,
|
required B newValue,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
extension ValueNotifierBoolExtensions on ValueNotifier<bool> {
|
extension ValueNotifierBoolExtensions on ValueNotifier<bool> {
|
||||||
void flipValue() => value = !value;
|
void flipValue() => value = !value;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mc_gallery/locator.dart';
|
|
||||||
|
|
||||||
|
import '/locator.dart';
|
||||||
import 'logging_service.dart';
|
import 'logging_service.dart';
|
||||||
|
|
||||||
typedef AddLifeCycleListener = void Function({
|
typedef AddLifeCycleListener = void Function({
|
||||||
|
@ -23,7 +23,8 @@ class AppLifecycleService with WidgetsBindingObserver {
|
||||||
|
|
||||||
final LoggingService _loggingService;
|
final LoggingService _loggingService;
|
||||||
|
|
||||||
late final StreamController<AppLifecycleState> _streamController = StreamController.broadcast();
|
late final StreamController<AppLifecycleState> _lifecycleStateStreamController =
|
||||||
|
StreamController.broadcast();
|
||||||
final Map<String, StreamSubscription> _appLifecycleSubscriptions = {};
|
final Map<String, StreamSubscription> _appLifecycleSubscriptions = {};
|
||||||
|
|
||||||
AppLifecycleState? _appLifeCycleState;
|
AppLifecycleState? _appLifeCycleState;
|
||||||
|
@ -43,14 +44,15 @@ class AppLifecycleService with WidgetsBindingObserver {
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
try {
|
try {
|
||||||
_appLifeCycleState = state;
|
_appLifeCycleState = state;
|
||||||
_streamController.add(state);
|
_lifecycleStateStreamController.add(state);
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
_loggingService.error(
|
_loggingService.error(
|
||||||
'Something went wrong logging ${state.name} inside the didChangeAppLifeCycleState method',
|
'Something went wrong with ${state.name} inside the didChangeAppLifeCycleState method',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,17 +63,17 @@ class AppLifecycleService with WidgetsBindingObserver {
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
if (_appLifecycleSubscriptions.containsKey(tag)) {
|
if (_appLifecycleSubscriptions.containsKey(tag)) {
|
||||||
_loggingService.warning('Tag already active, returning!');
|
_loggingService.warning('Tag already active, returning');
|
||||||
} else {
|
} else {
|
||||||
final message = 'Adding $tag appLifecycleState listener';
|
final message = 'Adding $tag appLifecycleState listener';
|
||||||
_loggingService.info('$message..');
|
_loggingService.info('$message..');
|
||||||
if (_appLifeCycleState != null && tryCallListenerOnAdd) listener(_appLifeCycleState!);
|
if (_appLifeCycleState != null && tryCallListenerOnAdd) listener(_appLifeCycleState!);
|
||||||
_appLifecycleSubscriptions[tag] = _streamController.stream.listen(listener);
|
_appLifecycleSubscriptions[tag] = _lifecycleStateStreamController.stream.listen(listener);
|
||||||
_loggingService.good('$message success!');
|
_loggingService.good('$message success!');
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
_loggingService.error(
|
_loggingService.error(
|
||||||
'Something went wrong adding $tag appLifecycleState listener!',
|
'Something went wrong adding $tag appLifecycleState listener',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
);
|
);
|
||||||
|
@ -88,11 +90,11 @@ class AppLifecycleService with WidgetsBindingObserver {
|
||||||
_appLifecycleSubscriptions.remove(tag);
|
_appLifecycleSubscriptions.remove(tag);
|
||||||
_loggingService.good('$message success!');
|
_loggingService.good('$message success!');
|
||||||
} else {
|
} else {
|
||||||
_loggingService.warning('Subscription was not found!');
|
_loggingService.warning('Subscription was not found');
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
_loggingService.error(
|
_loggingService.error(
|
||||||
'Something went wrong removing $tag appLifecycleState listener!',
|
'Something went wrong removing $tag appLifecycleState listener',
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||||
|
|
||||||
import '/features/core/services/logging_service.dart';
|
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
|
import 'logging_service.dart';
|
||||||
|
|
||||||
/// Used to observe the current connection type.
|
/// Used to observe the current connection type and status.
|
||||||
class ConnectionsService {
|
class ConnectionsService {
|
||||||
ConnectionsService({
|
ConnectionsService({
|
||||||
required Connectivity connectivity,
|
required Connectivity connectivity,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
|
||||||
import 'package:mc_gallery/locator.dart';
|
|
||||||
|
|
||||||
|
import '/locator.dart';
|
||||||
|
import 'logging_service.dart';
|
||||||
|
|
||||||
|
/// Handles storing state data locally, onto the device itself
|
||||||
class LocalStorageService {
|
class LocalStorageService {
|
||||||
LocalStorageService() {
|
LocalStorageService() {
|
||||||
_init();
|
_init();
|
||||||
|
@ -9,10 +11,10 @@ class LocalStorageService {
|
||||||
|
|
||||||
final LoggingService _loggingService = LoggingService.locate;
|
final LoggingService _loggingService = LoggingService.locate;
|
||||||
|
|
||||||
static const String _userBoxKey = 'userBoxKey';
|
|
||||||
|
|
||||||
late final Box<bool> _userBox;
|
late final Box<bool> _userBox;
|
||||||
|
|
||||||
|
static const String _userBoxKey = 'userBoxKey';
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_userBox = await Hive.openBox(_userBoxKey);
|
_userBox = await Hive.openBox(_userBoxKey);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
|
||||||
|
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
|
|
||||||
|
/// Handles logging of events.
|
||||||
class LoggingService {
|
class LoggingService {
|
||||||
final Talker _talker = Talker(
|
final Talker _talker = Talker(
|
||||||
settings: TalkerSettings(
|
settings: TalkerSettings(
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view.dart';
|
|
||||||
import 'package:mc_gallery/locator.dart';
|
|
||||||
|
|
||||||
|
import '/features/home/views/image_carousel/image_carousel_view.dart';
|
||||||
|
import '/locator.dart';
|
||||||
import '../abstracts/router/app_router.dart';
|
import '../abstracts/router/app_router.dart';
|
||||||
import '../abstracts/router/routes.dart';
|
import '../abstracts/router/routes.dart';
|
||||||
|
|
||||||
|
/// Handles the navigation to and fro all the screens in the app.
|
||||||
class NavigationService {
|
class NavigationService {
|
||||||
const NavigationService({
|
const NavigationService({
|
||||||
required McgRouter mcgRouter,
|
required McgRouter mcgRouter,
|
||||||
|
@ -22,10 +23,5 @@ class NavigationService {
|
||||||
extra: imageCarouselViewArguments,
|
extra: imageCarouselViewArguments,
|
||||||
);
|
);
|
||||||
|
|
||||||
void backToGallery(BuildContext context) => context.pop();
|
|
||||||
|
|
||||||
void previous() {}
|
|
||||||
void next() {}
|
|
||||||
|
|
||||||
static NavigationService get locate => Locator.locate();
|
static NavigationService get locate => Locator.locate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
|
||||||
import 'package:mc_gallery/locator.dart';
|
import '/locator.dart';
|
||||||
|
import 'logging_service.dart';
|
||||||
|
|
||||||
class OverlayService {
|
class OverlayService {
|
||||||
OverlayService({
|
OverlayService({
|
||||||
|
|
|
@ -20,6 +20,7 @@ class Mutex {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allows listening to the completion status of the last worker process to be released.
|
||||||
Future<void> get lastOperationCompletionAwaiter =>
|
Future<void> get lastOperationCompletionAwaiter =>
|
||||||
_completerQueue.isNotEmpty ? _completerQueue.last.future : Future.value();
|
_completerQueue.isNotEmpty ? _completerQueue.last.future : Future.value();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '/l10n/generated/l10n.dart';
|
||||||
import '../widgets/gap.dart';
|
import '../widgets/gap.dart';
|
||||||
|
|
||||||
class ErrorPageView extends StatelessWidget {
|
class ErrorPageView extends StatelessWidget {
|
||||||
|
@ -15,9 +16,8 @@ class ErrorPageView extends StatelessWidget {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Text('Oopsie, there has been an error. Hang tight till we do something about it.'),
|
Text(Strings.current.errorPageMessage),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('what happened: $error'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '/features/core/data/constants/const_durations.dart';
|
||||||
|
|
||||||
|
/// [AnimatedColumn] Animates its children when they get added or removed at the end
|
||||||
|
class AnimatedColumn extends StatelessWidget {
|
||||||
|
const AnimatedColumn({
|
||||||
|
required this.children,
|
||||||
|
this.duration = ConstDurations.oneAndHalfDefaultAnimationDuration,
|
||||||
|
this.mainAxisAlignment = MainAxisAlignment.start,
|
||||||
|
this.mainAxisSize = MainAxisSize.max,
|
||||||
|
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||||
|
this.textDirection,
|
||||||
|
this.verticalDirection = VerticalDirection.down,
|
||||||
|
this.textBaseline,
|
||||||
|
this.maxAnimatingChildren = 2,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// [duration] specifies the duration of the add/remove animation
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
/// [maxAnimatingChildren] determines the maximum number of chidren that can
|
||||||
|
/// be animating at once, if more are removed or added at within an animation
|
||||||
|
/// duration they will pop in instead
|
||||||
|
final int maxAnimatingChildren;
|
||||||
|
|
||||||
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
|
final MainAxisSize mainAxisSize;
|
||||||
|
final CrossAxisAlignment crossAxisAlignment;
|
||||||
|
final TextDirection? textDirection;
|
||||||
|
final VerticalDirection verticalDirection;
|
||||||
|
final TextBaseline? textBaseline;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < children.length + maxAnimatingChildren; i++)
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: duration,
|
||||||
|
switchInCurve: Curves.easeInOut,
|
||||||
|
switchOutCurve: Curves.easeInOut,
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
sizeFactor: animation,
|
||||||
|
axisAlignment: -1,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: i < children.length ? children[i] : const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ 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
|
/// 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 {
|
||||||
|
/// Returns images fetched through an API as [ImageModelDTO]s.
|
||||||
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token});
|
FutureOr<Iterable<ImageModelDTO>> fetchImageUri({required String token});
|
||||||
|
|
||||||
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
FutureOr<Iterable<ImageModelDTO>> searchImages({
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import '/l10n/generated/l10n.dart';
|
import '/l10n/generated/l10n.dart';
|
||||||
|
|
||||||
|
/// Represents an option for specifying a search strategy, for an [ImageModel]
|
||||||
enum SearchOption {
|
enum SearchOption {
|
||||||
local,
|
local,
|
||||||
web;
|
web;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import '../dtos/image_model_dto.dart';
|
import '../dtos/image_model_dto.dart';
|
||||||
|
|
||||||
|
/// Represents an Image, that would be displayed in the gallery.
|
||||||
class ImageModel {
|
class ImageModel {
|
||||||
const ImageModel({
|
const ImageModel({
|
||||||
required this.uri,
|
required this.uri,
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import 'dart:ui';
|
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/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(
|
ImageCacheManagerService(
|
||||||
{required AppLifecycleService appLifecycleService,
|
{required AppLifecycleService appLifecycleService,
|
||||||
required LocalStorageService localStorageService})
|
required LocalStorageService localStorageService})
|
||||||
|
@ -17,6 +19,7 @@ class ImageCacheManagerService with LoggingService {
|
||||||
|
|
||||||
final AppLifecycleService _appLifecycleService;
|
final AppLifecycleService _appLifecycleService;
|
||||||
final LocalStorageService _localStorageService;
|
final LocalStorageService _localStorageService;
|
||||||
|
final LoggingService _loggingService = LoggingService.locate;
|
||||||
final _cacheManager = DefaultCacheManager();
|
final _cacheManager = DefaultCacheManager();
|
||||||
|
|
||||||
Future<void> emptyCache() async => await _cacheManager.emptyCache();
|
Future<void> emptyCache() async => await _cacheManager.emptyCache();
|
||||||
|
@ -31,7 +34,7 @@ class ImageCacheManagerService with LoggingService {
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
info('Discarding cached images');
|
_loggingService.info('Discarding cached images');
|
||||||
await _cacheManager.emptyCache();
|
await _cacheManager.emptyCache();
|
||||||
_localStorageService.resetFavourites();
|
_localStorageService.resetFavourites();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:collection/collection.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';
|
||||||
|
@ -11,6 +10,7 @@ import '/features/core/data/extensions/string_extensions.dart';
|
||||||
import '/features/core/services/local_storage_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/utils/mutex.dart';
|
import '/features/core/utils/mutex.dart';
|
||||||
|
import '/features/home/data/dtos/image_model_dto.dart';
|
||||||
import '/locator.dart';
|
import '/locator.dart';
|
||||||
import '../abstracts/images_api.dart';
|
import '../abstracts/images_api.dart';
|
||||||
import '../data/enums/search_option.dart';
|
import '../data/enums/search_option.dart';
|
||||||
|
|
|
@ -18,12 +18,7 @@ class _DownloadedGalleryView extends StatelessWidget {
|
||||||
child: ValueListenableBuilder<bool>(
|
child: ValueListenableBuilder<bool>(
|
||||||
valueListenable: galleryViewModel.isViewingFavouriteListenable,
|
valueListenable: galleryViewModel.isViewingFavouriteListenable,
|
||||||
builder: (context, final isViewingFavourites, _) => !isViewingFavourites
|
builder: (context, final isViewingFavourites, _) => !isViewingFavourites
|
||||||
? Wrap(
|
? CustomWrap(
|
||||||
runSpacing: 24,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
for (final imageModel in galleryViewModel.imageModels)
|
for (final imageModel in galleryViewModel.imageModels)
|
||||||
_StarrableImage(
|
_StarrableImage(
|
||||||
|
@ -33,12 +28,7 @@ class _DownloadedGalleryView extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: Wrap(
|
: CustomWrap(
|
||||||
runSpacing: 24,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
for (final favouriteImageModel in galleryViewModel.favouriteImageModels)
|
for (final favouriteImageModel in galleryViewModel.favouriteImageModels)
|
||||||
_StarrableImage(
|
_StarrableImage(
|
||||||
|
@ -88,11 +78,14 @@ class _StarrableImageState extends State<_StarrableImage> {
|
||||||
context,
|
context,
|
||||||
imageModel: widget.imageModel,
|
imageModel: widget.imageModel,
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: Hero(
|
||||||
imageUrl: widget.imageModel.uri.toString(),
|
tag: widget.imageModel.imageIndex,
|
||||||
cacheKey: widget.imageModel.imageIndex.toString(),
|
child: CachedNetworkImage(
|
||||||
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
|
imageUrl: widget.imageModel.uri.toString(),
|
||||||
value: widget.galleryViewModel.downloadProgressValue(progress: progress),
|
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: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 '/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';
|
||||||
|
import '/features/core/data/constants/const_media.dart';
|
||||||
import '/features/core/widgets/gap.dart';
|
import '/features/core/widgets/gap.dart';
|
||||||
import '/features/core/widgets/mcg_scaffold.dart';
|
import '/features/core/widgets/mcg_scaffold.dart';
|
||||||
import '/features/core/widgets/state/multi_value_listenable_builder.dart';
|
import '/features/core/widgets/state/multi_value_listenable_builder.dart';
|
||||||
import '/features/core/widgets/state/view_model_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/enums/search_option.dart';
|
||||||
import '../../data/models/image_model.dart';
|
import '../../data/models/image_model.dart';
|
||||||
import 'gallery_view_model.dart';
|
import 'gallery_view_model.dart';
|
||||||
|
@ -81,21 +82,38 @@ class GalleryView extends StatelessWidget {
|
||||||
valueListenable: model.isSearchingListenable,
|
valueListenable: model.isSearchingListenable,
|
||||||
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
builder: (context, final isSearching, _) => AnimatedSwitcher(
|
||||||
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
duration: ConstDurations.oneAndHalfDefaultAnimationDuration,
|
||||||
child: Column(
|
child: !isSearching
|
||||||
children: [
|
? Column(
|
||||||
ValueListenableBuilder<bool>(
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
valueListenable: model.isViewingFavouriteListenable,
|
children: [
|
||||||
builder: (context, final isViewingFavourites, child) =>
|
ValueListenableBuilder<bool>(
|
||||||
Switch(
|
valueListenable: model.isViewingFavouriteListenable,
|
||||||
value: isViewingFavourites,
|
builder:
|
||||||
onChanged: model.onFavouriteViewChange,
|
(context, final isViewingFavourites, child) =>
|
||||||
),
|
Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
!isSearching
|
children: [
|
||||||
? _DownloadedGalleryView(galleryViewModel: model)
|
ConstMedia.buildIcon(
|
||||||
: _SearchGalleryView(galleryViewModel: model),
|
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,9 +3,9 @@ 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/data/extensions/value_notifier_extensions.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 '/locator.dart';
|
import '/locator.dart';
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -61,13 +61,16 @@ class ImageCarouselView extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
ValueListenableBuilder<ImageModel>(
|
ValueListenableBuilder<ImageModel>(
|
||||||
valueListenable: model.currentImageModelListenable,
|
valueListenable: model.currentImageModelListenable,
|
||||||
builder: (context, _, __) => CachedNetworkImage(
|
builder: (context, _, __) => Hero(
|
||||||
imageUrl: model.currentImageUrl,
|
tag: model.currentImageIndex,
|
||||||
cacheKey: model.currentImageKey,
|
child: CachedNetworkImage(
|
||||||
fit: BoxFit.contain,
|
imageUrl: model.currentImageUrl,
|
||||||
progressIndicatorBuilder: (_, __, final progress) =>
|
cacheKey: model.currentImageKey,
|
||||||
CircularProgressIndicator(
|
fit: BoxFit.contain,
|
||||||
value: model.downloadProgressValue(progress: progress),
|
progressIndicatorBuilder: (_, __, final progress) =>
|
||||||
|
CircularProgressIndicator(
|
||||||
|
value: model.downloadProgressValue(progress: progress),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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/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';
|
||||||
|
@ -12,14 +11,11 @@ import '../../data/models/image_model.dart';
|
||||||
class ImageCarouselViewModel extends BaseViewModel {
|
class ImageCarouselViewModel extends BaseViewModel {
|
||||||
ImageCarouselViewModel({
|
ImageCarouselViewModel({
|
||||||
required ImagesService imagesService,
|
required ImagesService imagesService,
|
||||||
required NavigationService navigationService,
|
|
||||||
required LoggingService loggingService,
|
required LoggingService loggingService,
|
||||||
}) : _imagesService = imagesService,
|
}) : _imagesService = imagesService,
|
||||||
_navigationService = navigationService,
|
|
||||||
_loggingService = loggingService;
|
_loggingService = loggingService;
|
||||||
|
|
||||||
final ImagesService _imagesService;
|
final ImagesService _imagesService;
|
||||||
final NavigationService _navigationService;
|
|
||||||
final LoggingService _loggingService;
|
final LoggingService _loggingService;
|
||||||
|
|
||||||
late final ValueNotifier<ImageModel> _currentImageModelNotifier;
|
late final ValueNotifier<ImageModel> _currentImageModelNotifier;
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||||
|
|
||||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
|
"appTitle": MessageLookupByLibrary.simpleMessage("MC Gallery App"),
|
||||||
|
"errorPageMessage": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Oopsie, there has been an error. Hang tight till we do something about it."),
|
||||||
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
|
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
|
||||||
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
|
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
|
||||||
"imageDetails": MessageLookupByLibrary.simpleMessage(
|
"imageDetails": MessageLookupByLibrary.simpleMessage(
|
||||||
|
|
|
@ -50,6 +50,16 @@ class Strings {
|
||||||
return Localizations.of<Strings>(context, Strings);
|
return Localizations.of<Strings>(context, Strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `MC Gallery App`
|
||||||
|
String get appTitle {
|
||||||
|
return Intl.message(
|
||||||
|
'MC Gallery App',
|
||||||
|
name: 'appTitle',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Something went wrong`
|
/// `Something went wrong`
|
||||||
String get somethingWentWrong {
|
String get somethingWentWrong {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
@ -60,6 +70,16 @@ class Strings {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Oopsie, there has been an error. Hang tight till we do something about it.`
|
||||||
|
String get errorPageMessage {
|
||||||
|
return Intl.message(
|
||||||
|
'Oopsie, there has been an error. Hang tight till we do something about it.',
|
||||||
|
name: 'errorPageMessage',
|
||||||
|
desc: '',
|
||||||
|
args: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `Image {imageNumber}: size={imageSide}`
|
/// `Image {imageNumber}: size={imageSide}`
|
||||||
String imageNameFetch(Object imageNumber, Object imageSide) {
|
String imageNameFetch(Object imageNumber, Object imageSide) {
|
||||||
return Intl.message(
|
return Intl.message(
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
|
"appTitle": "MC Gallery App",
|
||||||
"somethingWentWrong": "Something went wrong",
|
"somethingWentWrong": "Something went wrong",
|
||||||
|
"errorPageMessage": "Oopsie, there has been an error. Hang tight till we do something about it.",
|
||||||
|
|
||||||
"imageNameFetch": "Image {imageNumber}: size={imageSide}",
|
"imageNameFetch": "Image {imageNumber}: size={imageSide}",
|
||||||
"imageNameSearch": "Search term '{searchStr}' result: Image {imageNumber}",
|
"imageNameSearch": "Search term '{searchStr}' result: Image {imageNumber}",
|
||||||
|
|
|
@ -53,7 +53,6 @@ class Locator {
|
||||||
instance().registerFactory(
|
instance().registerFactory(
|
||||||
() => ImageCarouselViewModel(
|
() => ImageCarouselViewModel(
|
||||||
imagesService: ImagesService.locate,
|
imagesService: ImagesService.locate,
|
||||||
navigationService: NavigationService.locate,
|
|
||||||
loggingService: LoggingService.locate,
|
loggingService: LoggingService.locate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue