ui backbone

This commit is contained in:
Mehul Ahal 2022-12-20 21:52:24 +01:00
parent 3e374d24f6
commit b7045fc242
24 changed files with 918 additions and 73 deletions

View file

@ -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'],
);
}

View file

@ -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;

View file

@ -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: '');

View file

@ -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),
),
),
),
),
],
),
),
),
),
),
),
),
);
}
}

View file

@ -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;
}

View file

@ -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),
),
),
),
),
);
}
}

View file

@ -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();
}