ui backbone
This commit is contained in:
parent
3e374d24f6
commit
b7045fc242
24 changed files with 918 additions and 73 deletions
|
@ -12,6 +12,8 @@ abstract class AppSetup {
|
|||
static Future<void> initialise() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await _setupStrings();
|
||||
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
|
@ -38,7 +40,7 @@ abstract class AppSetup {
|
|||
return supportedLocales.first;
|
||||
}
|
||||
|
||||
static Future<void> setupStrings() async {
|
||||
static Future<void> _setupStrings() async {
|
||||
await Strings.load(resolveLocale(WidgetsBinding.instance.window.locales, supportedLocales));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:mc_gallery/features/core/services/logging_service.dart';
|
||||
import 'package:mc_gallery/l10n/generated/l10n.dart';
|
||||
|
||||
import '/l10n/generated/l10n.dart';
|
||||
import '../data/enums/view_model_state.dart';
|
||||
import '../services/logging_service.dart';
|
||||
|
||||
abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
|
||||
abstract class BaseViewModel<T extends Object?> extends ChangeNotifier {
|
||||
final ValueNotifier<bool> _isInitialised = ValueNotifier(false);
|
||||
ValueListenable<bool> get isInitialised => _isInitialised;
|
||||
|
||||
|
@ -23,12 +23,11 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
|
|||
String get errorMessage => _errorMessage ?? strings.somethingWentWrong;
|
||||
|
||||
@mustCallSuper
|
||||
void initialise(DisposableBuildContext disposableBuildContext, bool Function() mounted,
|
||||
[E? arguments]) {
|
||||
void initialise(bool Function() mounted, [T? arguments]) {
|
||||
_mounted = mounted;
|
||||
_isInitialised.value = true;
|
||||
_state.value = ViewModelState.isInitialised;
|
||||
_loggingService.successfulInit(location: runtimeType.runtimeType.toString());
|
||||
_loggingService.successfulInit(location: runtimeType.toString());
|
||||
}
|
||||
|
||||
void setBusy(bool isBusy) {
|
||||
|
@ -56,7 +55,7 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
|
|||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_loggingService.successfulDispose(location: runtimeType.runtimeType.toString());
|
||||
_loggingService.successfulDispose(location: runtimeType.toString());
|
||||
}
|
||||
|
||||
late final bool Function() _mounted;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.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 '../../views/error_page_view.dart';
|
||||
import 'routes.dart';
|
||||
|
@ -14,12 +15,21 @@ class McgRouter {
|
|||
key: state.pageKey,
|
||||
child: ErrorPageView(error: state.error),
|
||||
),
|
||||
// TODO Add Redirect
|
||||
//todo(mehul): Add Redirect
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.home.routePath,
|
||||
name: Routes.home.routeName,
|
||||
builder: (context, _) => const GalleryView(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: Routes.imageCarousel.routePath,
|
||||
name: Routes.imageCarousel.routeName,
|
||||
builder: (context, state) => ImageCarouselView(
|
||||
imageCarouselViewArguments: state.extra as ImageCarouselViewArguments,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ enum Routes {
|
|||
routeName: 'Home',
|
||||
)),
|
||||
imageCarousel(RoutesInfo(
|
||||
routePath: '/image_carousel',
|
||||
routePath: 'image_carousel',
|
||||
routeName: 'Image Carousel',
|
||||
));
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
abstract class ConstColours {
|
||||
/// Smoke Gray => a neutral grey with neutral warmth/cold
|
||||
static const imageBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
|
||||
static const galleryBackgroundColour = Color.fromRGBO(127, 127, 125, 1.0);
|
||||
|
||||
static const white = Colors.white;
|
||||
static const black = Colors.black;
|
||||
|
|
|
@ -4,6 +4,6 @@ abstract class ConstValues {
|
|||
static const List<String> backendUrlPathSegments = ['user', 'c_v_r'];
|
||||
|
||||
static const int numberOfImages = 20;
|
||||
static const int minImageSize = 100;
|
||||
static const int maxImageSize = 250;
|
||||
static const int minImageSize = 50;
|
||||
static const int maxImageSize = 100;
|
||||
}
|
||||
|
|
103
lib/features/core/services/app_lifecycle_service.dart
Normal file
103
lib/features/core/services/app_lifecycle_service.dart
Normal file
|
@ -0,0 +1,103 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mc_gallery/locator.dart';
|
||||
|
||||
import 'logging_service.dart';
|
||||
|
||||
typedef AddLifeCycleListener = void Function({
|
||||
required void Function(AppLifecycleState appLifecycleState) listener,
|
||||
required String tag,
|
||||
bool tryCallListenerOnAdd,
|
||||
});
|
||||
typedef RemoveLifeCycleListener = Future<void> Function({required String tag});
|
||||
|
||||
/// Used to observe the current app lifecycle state.
|
||||
class AppLifecycleService with WidgetsBindingObserver {
|
||||
AppLifecycleService({
|
||||
required LoggingService loggingService,
|
||||
}) : _loggingService = loggingService {
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_appLifeCycleState = WidgetsBinding.instance.lifecycleState;
|
||||
}
|
||||
|
||||
final LoggingService _loggingService;
|
||||
|
||||
late final StreamController<AppLifecycleState> _streamController = StreamController.broadcast();
|
||||
final Map<String, StreamSubscription> _appLifecycleSubscriptions = {};
|
||||
|
||||
AppLifecycleState? _appLifeCycleState;
|
||||
AppLifecycleState? get appLifeCycleState => _appLifeCycleState;
|
||||
|
||||
Future<void> dispose() async {
|
||||
_loggingService.info('Disposing app lifecycle service..');
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
for (final subscription in _appLifecycleSubscriptions.values) {
|
||||
await subscription.cancel();
|
||||
}
|
||||
_appLifecycleSubscriptions.clear();
|
||||
_loggingService.good('App lifecycle service disposed!');
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
try {
|
||||
_appLifeCycleState = state;
|
||||
_streamController.add(state);
|
||||
} catch (error, stackTrace) {
|
||||
_loggingService.error(
|
||||
'Something went wrong logging ${state.name} inside the didChangeAppLifeCycleState method',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
super.didChangeAppLifecycleState(state);
|
||||
}
|
||||
|
||||
void addListener({
|
||||
required void Function(AppLifecycleState appLifecycleState) listener,
|
||||
required String tag,
|
||||
bool tryCallListenerOnAdd = true,
|
||||
}) {
|
||||
try {
|
||||
if (_appLifecycleSubscriptions.containsKey(tag)) {
|
||||
_loggingService.warning('Tag already active, returning!');
|
||||
} else {
|
||||
final message = 'Adding $tag appLifecycleState listener';
|
||||
_loggingService.info('$message..');
|
||||
if (_appLifeCycleState != null && tryCallListenerOnAdd) listener(_appLifeCycleState!);
|
||||
_appLifecycleSubscriptions[tag] = _streamController.stream.listen(listener);
|
||||
_loggingService.good('$message success!');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_loggingService.error(
|
||||
'Something went wrong adding $tag appLifecycleState listener!',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeListener({required String tag}) async {
|
||||
try {
|
||||
final message = 'Removing $tag appLifecycleState listener';
|
||||
_loggingService.info('$message..');
|
||||
final subscription = _appLifecycleSubscriptions[tag];
|
||||
if (subscription != null) {
|
||||
await subscription.cancel();
|
||||
_appLifecycleSubscriptions.remove(tag);
|
||||
_loggingService.good('$message success!');
|
||||
} else {
|
||||
_loggingService.warning('Subscription was not found!');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_loggingService.error(
|
||||
'Something went wrong removing $tag appLifecycleState listener!',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static AppLifecycleService get locate => Locator.locate();
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/widgets.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 '../abstracts/router/app_router.dart';
|
||||
import '../abstracts/router/routes.dart';
|
||||
|
@ -11,11 +13,19 @@ class NavigationService {
|
|||
|
||||
final McgRouter _mcgRouter;
|
||||
|
||||
void pushImageCarouselView(BuildContext context) =>
|
||||
context.pushNamed(Routes.imageCarousel.routeName);
|
||||
void pushImageCarouselView(
|
||||
BuildContext context, {
|
||||
required ImageCarouselViewArguments imageCarouselViewArguments,
|
||||
}) =>
|
||||
context.pushNamed(
|
||||
Routes.imageCarousel.routeName,
|
||||
extra: imageCarouselViewArguments,
|
||||
);
|
||||
|
||||
void backToGallery(BuildContext context) => context.pop();
|
||||
|
||||
void previous() {}
|
||||
void next() {}
|
||||
|
||||
static NavigationService get locate => Locator.locate();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -9,7 +8,7 @@ import '../services/connection_service.dart';
|
|||
class McgScaffold extends StatelessWidget {
|
||||
const McgScaffold({
|
||||
this.appBar,
|
||||
this.bodyBuilderCompleter,
|
||||
this.bodyBuilderWaiter,
|
||||
this.body,
|
||||
this.waitingWidget,
|
||||
this.forceInternetCheck = false,
|
||||
|
@ -19,10 +18,10 @@ class McgScaffold extends StatelessWidget {
|
|||
final AppBar? appBar;
|
||||
|
||||
/// Awaits an external signal (complete) before building the body.
|
||||
final Completer? bodyBuilderCompleter;
|
||||
final ValueListenable<bool>? bodyBuilderWaiter;
|
||||
final Widget? body;
|
||||
|
||||
/// Custom widget to be used while awaiting [bodyBuilderCompleter].
|
||||
/// Custom widget to be used while awaiting [bodyBuilderWaiter].
|
||||
///
|
||||
/// Defaults to using [PlatformCircularProgressIndicator].
|
||||
final Widget? waitingWidget;
|
||||
|
@ -32,19 +31,12 @@ class McgScaffold extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget loginOptionsBody = bodyBuilderCompleter != null
|
||||
? FutureBuilder(
|
||||
future: bodyBuilderCompleter!.future,
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.none:
|
||||
case ConnectionState.waiting:
|
||||
case ConnectionState.active:
|
||||
return Center(child: waitingWidget ?? const CircularProgressIndicator());
|
||||
case ConnectionState.done:
|
||||
return body ?? const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
final Widget loginOptionsBody = bodyBuilderWaiter != null
|
||||
? ValueListenableBuilder<bool>(
|
||||
valueListenable: bodyBuilderWaiter!,
|
||||
builder: (context, final isReady, child) => !isReady
|
||||
? Center(child: waitingWidget ?? const CircularProgressIndicator())
|
||||
: body ?? const SizedBox.shrink(),
|
||||
)
|
||||
: body ?? const SizedBox.shrink();
|
||||
|
||||
|
|
47
lib/features/core/widgets/view_model_builder.dart
Normal file
47
lib/features/core/widgets/view_model_builder.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../abstracts/base_view_model.dart';
|
||||
|
||||
class ViewModelBuilder<T extends BaseViewModel> extends StatefulWidget {
|
||||
const ViewModelBuilder({
|
||||
required Widget Function(BuildContext context, T model) builder,
|
||||
required T Function() viewModelBuilder,
|
||||
dynamic Function()? argumentBuilder,
|
||||
super.key,
|
||||
}) : _builder = builder,
|
||||
_viewModelBuilder = viewModelBuilder,
|
||||
_argumentBuilder = argumentBuilder;
|
||||
|
||||
final Widget Function(BuildContext context, T model) _builder;
|
||||
final T Function() _viewModelBuilder;
|
||||
final dynamic Function()? _argumentBuilder;
|
||||
|
||||
@override
|
||||
_ViewModelBuilderState<T> createState() => _ViewModelBuilderState<T>();
|
||||
}
|
||||
|
||||
class _ViewModelBuilderState<T extends BaseViewModel> extends State<ViewModelBuilder<T>> {
|
||||
late final T _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_viewModel = widget._viewModelBuilder();
|
||||
_viewModel.initialise(() => mounted, widget._argumentBuilder?.call());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext _) => ChangeNotifierProvider.value(
|
||||
value: _viewModel,
|
||||
child: Consumer<T>(
|
||||
builder: (context, model, _) => widget._builder(context, model),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -18,8 +18,8 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
|
||||
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||
|
||||
return ImageModel<int>(
|
||||
comparableIndex: imageIndex,
|
||||
return ImageModel(
|
||||
imageIndex: imageIndex,
|
||||
uri: imageUri,
|
||||
imageName: Strings.current.image,
|
||||
);
|
||||
|
@ -29,6 +29,6 @@ class UnsplashImagesApi implements ImagesApi {
|
|||
Uri _imageUrlGenerator({required int imageSide}) => Uri(
|
||||
scheme: ConstValues.httpsScheme,
|
||||
host: ConstValues.backendHost,
|
||||
pathSegments: ConstValues.backendUrlPathSegments..add('${imageSide}x$imageSide'),
|
||||
pathSegments: [...ConstValues.backendUrlPathSegments, '${imageSide}x$imageSide'],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class ImageModel<T extends Comparable> {
|
||||
class ImageModel {
|
||||
const ImageModel({
|
||||
required this.uri,
|
||||
required this.comparableIndex,
|
||||
required this.imageIndex,
|
||||
required this.imageName,
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,7 @@ class ImageModel<T extends Comparable> {
|
|||
final Uri uri;
|
||||
|
||||
/// A unique identifier that can be used for indexing the image.
|
||||
final T comparableIndex;
|
||||
final int imageIndex;
|
||||
|
||||
/// Given name of the image.
|
||||
final String imageName;
|
||||
|
|
|
@ -15,6 +15,7 @@ class ImagesService {
|
|||
final ImagesApi _imagesApi;
|
||||
|
||||
late final Iterable<ImageModel> _imageModels;
|
||||
Iterable<ImageModel> get imageModels => _imageModels;
|
||||
|
||||
Future<void> _init() async {
|
||||
_imageModels = await _imagesApi.fetchImageUri(token: '');
|
||||
|
|
|
@ -1,10 +1,71 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '/features/core/data/constants/const_colors.dart';
|
||||
import '/features/core/data/constants/const_durations.dart';
|
||||
import '/features/core/widgets/mcg_scaffold.dart';
|
||||
import '/features/core/widgets/view_model_builder.dart';
|
||||
import 'gallery_view_model.dart';
|
||||
|
||||
class GalleryView extends StatelessWidget {
|
||||
const GalleryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
return ViewModelBuilder<GalleryViewModel>(
|
||||
viewModelBuilder: () => GalleryViewModel.locate,
|
||||
builder: (context, final model) => McgScaffold(
|
||||
bodyBuilderWaiter: model.isInitialised,
|
||||
appBar: AppBar(
|
||||
title: Text(model.strings.gallery),
|
||||
),
|
||||
body: Center(
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: model.isDisplayingPressingPrompt,
|
||||
builder: (context, final isDisplayingPressingPrompt, _) => AnimatedSwitcher(
|
||||
duration: ConstDurations.defaultAnimationDuration,
|
||||
child: isDisplayingPressingPrompt
|
||||
? ElevatedButton(
|
||||
onPressed: model.onPromptPressed,
|
||||
child: Text(model.strings.startLoadingPrompt),
|
||||
)
|
||||
: DecoratedBox(
|
||||
decoration: const BoxDecoration(color: ConstColours.galleryBackgroundColour),
|
||||
child: SingleChildScrollView(
|
||||
// Using Wrap instead of GridView, to make use of different image sizes
|
||||
child: Wrap(
|
||||
runSpacing: 24,
|
||||
spacing: 8,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final imageModel in model.imageModels)
|
||||
GestureDetector(
|
||||
onTap: () => model.pushImageCarouselView(
|
||||
context,
|
||||
imageModel: imageModel,
|
||||
),
|
||||
child: Hero(
|
||||
tag: imageModel.imageIndex.toString(),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageModel.uri.toString(),
|
||||
cacheKey: imageModel.imageIndex.toString(),
|
||||
progressIndicatorBuilder: (_, __, final progress) =>
|
||||
CircularProgressIndicator(
|
||||
value: model.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mc_gallery/features/core/services/app_lifecycle_service.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/images_service.dart';
|
||||
import '/features/home/views/image_carousel/image_carousel_view.dart';
|
||||
import '/locator.dart';
|
||||
|
||||
class GalleryViewModel extends BaseViewModel {
|
||||
GalleryViewModel({
|
||||
required ImagesService imagesService,
|
||||
required NavigationService navigationService,
|
||||
required AppLifecycleService appLifecycleService,
|
||||
required LoggingService loggingService,
|
||||
}) : _imagesService = imagesService,
|
||||
_navigationService = navigationService,
|
||||
_appLifecycleService = appLifecycleService,
|
||||
_loggingService = loggingService;
|
||||
|
||||
final ImagesService _imagesService;
|
||||
final NavigationService _navigationService;
|
||||
final AppLifecycleService _appLifecycleService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
final ValueNotifier<bool> _isDisplayingPressingPrompt = ValueNotifier(true);
|
||||
ValueListenable<bool> get isDisplayingPressingPrompt => _isDisplayingPressingPrompt;
|
||||
|
||||
@override
|
||||
Future<void> initialise(bool Function() mounted, [arguments]) async {
|
||||
_appLifecycleService.addListener(
|
||||
tag: runtimeType.toString(),
|
||||
listener: (final appLifecycleState) async {
|
||||
switch (appLifecycleState) {
|
||||
case AppLifecycleState.resumed:
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
await DefaultCacheManager().emptyCache();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
super.initialise(mounted, arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _appLifecycleService.removeListener(tag: runtimeType.toString());
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onPromptPressed() => _isDisplayingPressingPrompt.value = false;
|
||||
|
||||
Iterable<ImageModel> get imageModels => _imagesService.imageModels;
|
||||
|
||||
void pushImageCarouselView(BuildContext context, {required ImageModel imageModel}) =>
|
||||
_navigationService.pushImageCarouselView(
|
||||
context,
|
||||
imageCarouselViewArguments: ImageCarouselViewArguments(
|
||||
imageIndexKey: imageModel.imageIndex,
|
||||
),
|
||||
);
|
||||
|
||||
static GalleryViewModel get locate => Locator.locate();
|
||||
|
||||
double? downloadProgressValue({required DownloadProgress progress}) =>
|
||||
progress.totalSize != null ? progress.downloaded / progress.totalSize! : null;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '/features/core/widgets/mcg_scaffold.dart';
|
||||
import '/features/core/widgets/view_model_builder.dart';
|
||||
import '/features/home/views/image_carousel/image_carousel_view_model.dart';
|
||||
|
||||
class ImageCarouselViewArguments {
|
||||
const ImageCarouselViewArguments({required this.imageIndexKey});
|
||||
final int imageIndexKey;
|
||||
}
|
||||
|
||||
class ImageCarouselView extends StatelessWidget {
|
||||
const ImageCarouselView({
|
||||
required this.imageCarouselViewArguments,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ImageCarouselViewArguments imageCarouselViewArguments;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ImageCarouselViewModel>(
|
||||
viewModelBuilder: () => ImageCarouselViewModel.locate,
|
||||
argumentBuilder: () => imageCarouselViewArguments,
|
||||
builder: (context, final model) => McgScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(model.strings.imageCarousel),
|
||||
),
|
||||
body: Hero(
|
||||
tag: model.currentImageKey,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: model.currentImageUrl,
|
||||
cacheKey: model.currentImageKey,
|
||||
progressIndicatorBuilder: (_, __, final progress) => CircularProgressIndicator(
|
||||
value: model.downloadProgressValue(progress: progress),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:mc_gallery/features/core/services/app_lifecycle_service.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/images_service.dart';
|
||||
import '/features/home/views/image_carousel/image_carousel_view.dart';
|
||||
import '/locator.dart';
|
||||
|
||||
class ImageCarouselViewModel extends BaseViewModel {
|
||||
ImageCarouselViewModel({
|
||||
required ImagesService imagesService,
|
||||
required NavigationService navigationService,
|
||||
required AppLifecycleService appLifecycleService,
|
||||
required LoggingService loggingService,
|
||||
}) : _imagesService = imagesService,
|
||||
_navigationService = navigationService,
|
||||
_appLifecycleService = appLifecycleService,
|
||||
_loggingService = loggingService;
|
||||
|
||||
final ImagesService _imagesService;
|
||||
final NavigationService _navigationService;
|
||||
final AppLifecycleService _appLifecycleService;
|
||||
final LoggingService _loggingService;
|
||||
|
||||
late final ValueNotifier<ImageModel> _currentImageModel;
|
||||
ValueListenable<ImageModel> get currentImageModel => _currentImageModel;
|
||||
|
||||
@override
|
||||
Future<void> initialise(bool Function() mounted, [arguments]) async {
|
||||
_appLifecycleService.addListener(
|
||||
tag: runtimeType.toString(),
|
||||
listener: (final appLifecycleState) async {
|
||||
switch (appLifecycleState) {
|
||||
case AppLifecycleState.resumed:
|
||||
break;
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
await DefaultCacheManager().emptyCache();
|
||||
}
|
||||
});
|
||||
|
||||
_currentImageModel = ValueNotifier(_imagesService.imageModels
|
||||
.elementAt((arguments! as ImageCarouselViewArguments).imageIndexKey));
|
||||
|
||||
super.initialise(mounted, arguments);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await _appLifecycleService.removeListener(tag: runtimeType.toString());
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get currentImageUrl => currentImageModel.value.uri.toString();
|
||||
String get currentImageKey => currentImageModel.value.imageIndex.toString();
|
||||
double? downloadProgressValue({required DownloadProgress progress}) =>
|
||||
progress.totalSize != null ? progress.downloaded / progress.totalSize! : null;
|
||||
|
||||
static ImageCarouselViewModel get locate => Locator.locate();
|
||||
}
|
|
@ -22,8 +22,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
|
||||
"image": MessageLookupByLibrary.simpleMessage("Image"),
|
||||
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
|
||||
"somethingWentWrong":
|
||||
MessageLookupByLibrary.simpleMessage("Something went wrong")
|
||||
MessageLookupByLibrary.simpleMessage("Something went wrong"),
|
||||
"startLoadingPrompt":
|
||||
MessageLookupByLibrary.simpleMessage("Press me to start loading")
|
||||
};
|
||||
}
|
||||
|
|
|
@ -69,6 +69,36 @@ class Strings {
|
|||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Gallery`
|
||||
String get gallery {
|
||||
return Intl.message(
|
||||
'Gallery',
|
||||
name: 'gallery',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Press me to start loading`
|
||||
String get startLoadingPrompt {
|
||||
return Intl.message(
|
||||
'Press me to start loading',
|
||||
name: 'startLoadingPrompt',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Image carousel`
|
||||
String get imageCarousel {
|
||||
return Intl.message(
|
||||
'Image carousel',
|
||||
name: 'imageCarousel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<Strings> {
|
||||
|
|
|
@ -2,5 +2,10 @@
|
|||
"@@locale": "en",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
|
||||
"image": "Image"
|
||||
"image": "Image",
|
||||
|
||||
"gallery": "Gallery",
|
||||
"startLoadingPrompt": "Press me to start loading",
|
||||
|
||||
"imageCarousel": "Image carousel"
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||
|
@ -6,7 +8,10 @@ import 'package:mc_gallery/features/core/services/logging_service.dart';
|
|||
import 'package:mc_gallery/features/core/services/navigation_service.dart';
|
||||
import 'package:mc_gallery/features/home/api/unsplash_images_api.dart';
|
||||
import 'package:mc_gallery/features/home/services/images_service.dart';
|
||||
import 'package:mc_gallery/features/home/views/gallery/gallery_view_model.dart';
|
||||
import 'package:mc_gallery/features/home/views/image_carousel/image_carousel_view_model.dart';
|
||||
|
||||
import 'features/core/services/app_lifecycle_service.dart';
|
||||
import 'features/core/services/connection_service.dart';
|
||||
import 'features/core/services/overlay_service.dart';
|
||||
|
||||
|
@ -23,8 +28,8 @@ class Locator {
|
|||
_registerViewModels();
|
||||
|
||||
await _registerServices(locator);
|
||||
|
||||
await _registerRepos(locator);
|
||||
|
||||
_registerSingletons();
|
||||
}
|
||||
|
||||
|
@ -34,9 +39,26 @@ class Locator {
|
|||
);
|
||||
}
|
||||
|
||||
static void _registerViewModels() {}
|
||||
static void _registerViewModels() {
|
||||
instance().registerFactory(
|
||||
() => GalleryViewModel(
|
||||
imagesService: ImagesService.locate,
|
||||
navigationService: NavigationService.locate,
|
||||
appLifecycleService: AppLifecycleService.locate,
|
||||
loggingService: LoggingService.locate,
|
||||
),
|
||||
);
|
||||
instance().registerFactory(
|
||||
() => ImageCarouselViewModel(
|
||||
imagesService: ImagesService.locate,
|
||||
navigationService: NavigationService.locate,
|
||||
appLifecycleService: AppLifecycleService.locate,
|
||||
loggingService: LoggingService.locate,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static _registerServices(GetIt it) {
|
||||
static FutureOr<void> _registerServices(GetIt it) {
|
||||
it.registerLazySingleton(
|
||||
() => NavigationService(
|
||||
mcgRouter: McgRouter.locate,
|
||||
|
@ -57,6 +79,12 @@ class Locator {
|
|||
),
|
||||
dispose: (param) => param.dispose(),
|
||||
);
|
||||
instance().registerSingleton<AppLifecycleService>(
|
||||
AppLifecycleService(
|
||||
loggingService: LoggingService.locate,
|
||||
),
|
||||
dispose: (param) async => await param.dispose(),
|
||||
);
|
||||
it.registerSingleton(
|
||||
ImagesService(
|
||||
imagesApi: UnsplashImagesApi(),
|
||||
|
@ -65,7 +93,7 @@ class Locator {
|
|||
);
|
||||
}
|
||||
|
||||
static _registerRepos(GetIt locator) {}
|
||||
static FutureOr<void> _registerRepos(GetIt locator) {}
|
||||
|
||||
static void _registerSingletons() {}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue