This commit is contained in:
Mehul Ahal 2022-12-19 14:03:38 +01:00
commit 8e54cfffc5
91 changed files with 2686 additions and 0 deletions

View file

@ -0,0 +1,49 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '/l10n/generated/l10n.dart';
import '/locator.dart';
abstract class AppSetup {
// TODO: When locator is properly refactored we should not have to use these stub methods for testing
static Future<void> initialise() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
await Locator.setup();
}
static final List<Locale> supportedLocales = kReleaseMode
? <Locale>[
const Locale.fromSubtags(languageCode: 'en'),
]
: Strings.delegate.supportedLocales;
static Locale resolveLocale(List<Locale>? preferredLocales, Iterable<Locale> supportedLocales) {
for (final locale in preferredLocales ?? const <Locale>[]) {
// Check if the current device locale is supported
for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return supportedLocale;
}
}
}
return supportedLocales.first;
}
static Future<void> setupStrings() async {
await Strings.load(resolveLocale(WidgetsBinding.instance.window.locales, supportedLocales));
}
static void Function(Object error, StackTrace stackTrace) get onUncaughtException => (
error,
stackTrace,
) {};
}

View file

@ -0,0 +1,73 @@
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 '../data/enums/view_model_state.dart';
abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
final ValueNotifier<bool> _isInitialised = ValueNotifier(false);
ValueListenable<bool> get isInitialised => _isInitialised;
final ValueNotifier<bool> _isBusy = ValueNotifier(false);
ValueListenable<bool> get isBusy => _isBusy;
final ValueNotifier<bool> _hasError = ValueNotifier(false);
ValueListenable<bool> get hasError => _hasError;
final ValueNotifier<ViewModelState> _state = ValueNotifier(ViewModelState.isInitialising);
ValueListenable<ViewModelState> get state => _state;
final LoggingService _loggingService = LoggingService.locate;
String? _errorMessage;
String get errorMessage => _errorMessage ?? strings.somethingWentWrong;
@mustCallSuper
void initialise(DisposableBuildContext disposableBuildContext, bool Function() mounted,
[E? arguments]) {
_mounted = mounted;
_isInitialised.value = true;
_state.value = ViewModelState.isInitialised;
_loggingService.successfulInit(location: runtimeType.runtimeType.toString());
}
void setBusy(bool isBusy) {
_isBusy.value = isBusy;
if (isBusy) {
_state.value = ViewModelState.isBusy;
} else {
_state.value = ViewModelState.isInitialised;
}
}
void setError(
bool hasError, {
String? message,
}) {
_errorMessage = hasError ? message : null;
_hasError.value = hasError;
if (hasError) {
_state.value = ViewModelState.hasError;
} else {
_state.value = ViewModelState.isInitialised;
}
}
@override
void dispose() {
super.dispose();
_loggingService.successfulDispose(location: runtimeType.runtimeType.toString());
}
late final bool Function() _mounted;
void ifMounted(VoidCallback voidCallback) {
if (_mounted()) {
voidCallback();
}
}
final Strings strings = Strings.current;
double width(BuildContext context) => MediaQuery.of(context).size.width;
double height(BuildContext context) => MediaQuery.of(context).size.height;
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mc_gallery/features/home/views/gallery_view.dart';
import '../../views/error_page_view.dart';
import 'routes.dart';
class McgRouter {
static final _mcgRouter = McgRouter();
final router = GoRouter(
initialLocation: Routes.home.routePath,
debugLogDiagnostics: true,
errorPageBuilder: (context, state) => MaterialPage<void>(
key: state.pageKey,
child: ErrorPageView(error: state.error),
),
// TODO Add Redirect
routes: [
GoRoute(
path: Routes.home.routePath,
name: Routes.home.routeName,
builder: (context, _) => const GalleryView(),
),
],
);
static McgRouter get locate => _mcgRouter;
}

View file

@ -0,0 +1,27 @@
enum Routes {
home(RoutesInfo(
routePath: '/gallery',
routeName: 'Home',
)),
imageCarousel(RoutesInfo(
routePath: '/image_carousel',
routeName: 'Image Carousel',
));
final RoutesInfo _routeInfo;
String get routePath => _routeInfo.routePath;
String get routeName => _routeInfo.routeName;
const Routes(this._routeInfo);
}
class RoutesInfo {
final String routePath;
final String routeName;
const RoutesInfo({
required this.routePath,
required this.routeName,
});
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/cupertino.dart';
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 white = Colors.white;
static const black = Colors.black;
}
abstract class ConstThemes {
static ThemeData get materialLightTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorSchemeSeed: ConstColours.white,
);
static ThemeData get materialDarkTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: ConstColours.white,
);
static CupertinoThemeData get cupertinoLightTheme => const CupertinoThemeData(
brightness: Brightness.light,
);
static CupertinoThemeData get cupertinoDarkTheme => const CupertinoThemeData(
brightness: Brightness.dark,
);
static ThemeData get cupertinoThemeLightHack =>
materialLightTheme.copyWith(cupertinoOverrideTheme: cupertinoLightTheme);
static ThemeData get cupertinoThemeDarkHack =>
materialDarkTheme.copyWith(cupertinoOverrideTheme: cupertinoDarkTheme);
}

View file

@ -0,0 +1,6 @@
enum ViewModelState {
isInitialising,
isInitialised,
isBusy,
hasError;
}

View file

@ -0,0 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
extension DarkMode on BuildContext {
/// is dark mode currently enabled?
///
/// Using the context version in release throws exception.
bool get isDarkMode =>
WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark;
double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.height;
}

View file

@ -0,0 +1,6 @@
import 'package:dio/dio.dart';
extension ResponseExtensions on Response {
/// Shorthand for getting response's successful state.
bool get isSuccessful => (data as Map)['response'];
}

View file

@ -0,0 +1,3 @@
extension ListExtensions<T> on List<T> {
List<T> get deepCopy => [...this];
}

View file

@ -0,0 +1,3 @@
extension MapExtensions<A, B> on Map<A, B> {
Map<A, B> get deepCopy => {...this};
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/foundation.dart';
extension StreamExtensions<T> on Stream<T> {
ValueListenable<T> toValueNotifier(
T initialValue, {
bool Function(T previous, T current)? notifyWhen,
}) {
final notifier = ValueNotifier<T>(initialValue);
listen((value) {
if (notifyWhen == null || notifyWhen(notifier.value, value)) {
notifier.value = value;
}
});
return notifier;
}
ValueListenable<T?> toNullableValueNotifier({
bool Function(T? previous, T? current)? notifyWhen,
}) {
final notifier = ValueNotifier<T?>(null);
listen((value) {
if (notifyWhen == null || notifyWhen(notifier.value, value)) {
notifier.value = value;
}
});
return notifier;
}
Listenable toListenable() {
final notifier = ChangeNotifier();
listen((_) {
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
notifier.notifyListeners();
});
return notifier;
}
}

View file

@ -0,0 +1,66 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:talker/talker.dart';
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
import '/locator.dart';
class LoggingService {
final Talker _talker = Talker(
settings: TalkerSettings(
useHistory: false,
),
logger: TalkerLogger(formater: const ColoredLoggerFormatter()),
loggerSettings: TalkerLoggerSettings(
enableColors: !Platform.isIOS,
),
loggerOutput: debugPrint,
);
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get fine => _talker.fine;
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get good => _talker.good;
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get info => _talker.info;
void Function(dynamic msg, [Object exception, StackTrace stackTrace]) get warning =>
_talker.warning;
void Function(dynamic msg, [Object? exception, StackTrace? stackTrace]) get error =>
_talker.error;
void Function(Object exception, [StackTrace stackTrace, dynamic msg]) get handle =>
_talker.handle;
void Function(Error error, [StackTrace stackTrace, dynamic msg]) get handleError =>
_talker.handleError;
void Function(Exception exception, [StackTrace? stackTrace, dynamic msg]) get handleException =>
_talker.handleException;
void successfulInit({required String location}) => _talker.good('[$location] I am initialized');
void successfulDispose({required String location}) => _talker.good('[$location] I am disposed');
/// Adds logging to dio calls
void addLoggingInterceptor({required Dio dio}) {
dio.interceptors.add(
TalkerDioLogger(
talker: Talker(
logger: TalkerLogger(formater: const ColoredLoggerFormatter()),
settings: TalkerSettings(
useConsoleLogs: true,
useHistory: false,
),
loggerSettings: TalkerLoggerSettings(
enableColors: !Platform.isIOS,
),
),
settings: const TalkerDioLoggerSettings(
printRequestHeaders: true,
printResponseHeaders: true,
printResponseMessage: true,
),
),
);
}
static LoggingService get locate => Locator.locate();
}

View file

@ -0,0 +1,21 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import '../abstracts/router/app_router.dart';
import '../abstracts/router/routes.dart';
class NavigationService {
const NavigationService({
required McgRouter mcgRouter,
}) : _mcgRouter = mcgRouter;
final McgRouter _mcgRouter;
void pushImageCarouselView(BuildContext context) =>
context.pushNamed(Routes.imageCarousel.routeName);
void backToGallery(BuildContext context) => context.pop();
void previous() {}
void next() {}
}

View file

@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
import '../widgets/gap.dart';
class ErrorPageView extends StatelessWidget {
const ErrorPageView({
required this.error,
super.key,
});
final Exception? error;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
const Text('Oopsie, there has been an error. Hang tight till we do something about it.'),
const Gap(16),
Text('what happened: $error'),
],
),
);
}
}

View file

@ -0,0 +1,202 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class Gap extends LeafRenderObjectWidget {
const Gap(this.size, {Key? key}) : super(key: key);
final double size;
@override
RenderGap createRenderObject(BuildContext context) => RenderGap(size);
@override
void updateRenderObject(BuildContext context, covariant RenderGap renderObject) {
renderObject.gap = size;
super.updateRenderObject(context, renderObject);
}
static const size4 = Gap(4);
static const size8 = Gap(8);
static const size16 = Gap(16);
static const size24 = Gap(24);
static const size32 = Gap(32);
static const size64 = Gap(64);
}
class RenderGap extends RenderBox {
RenderGap(this._gap) : super() {
markNeedsLayout();
}
double _gap;
double get gap => _gap;
set gap(double value) {
if (_gap != value) {
_gap = value;
markNeedsLayout();
}
}
@override
void performLayout() {
final parent = this.parent;
Size newSize;
if (parent is RenderFlex) {
switch (parent.direction) {
case Axis.vertical:
newSize = Size(0, gap);
break;
case Axis.horizontal:
newSize = Size(gap, 0);
break;
}
} else {
newSize = Size.square(gap);
}
size = constraints.constrain(newSize);
}
}
class AnimatedGap extends StatefulWidget {
const AnimatedGap(
this.gap, {
Key? key,
this.duration = const Duration(milliseconds: 200),
this.curve = Curves.easeInOut,
}) : super(key: key);
final Duration duration;
final double gap;
final Curve curve;
@override
State<AnimatedGap> createState() => _AnimatedGapState();
}
class _AnimatedGapState extends State<AnimatedGap> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(
vsync: this,
value: widget.gap,
upperBound: double.infinity,
);
@override
void didUpdateWidget(covariant AnimatedGap oldWidget) {
if (oldWidget.gap != widget.gap) {
_controller.animateTo(
widget.gap,
curve: widget.curve,
duration: widget.duration,
);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<double>(
valueListenable: _controller,
builder: (context, gap, _) {
return Gap(gap);
},
);
}
}
class AnimatedSliverGap extends StatefulWidget {
const AnimatedSliverGap(
this.gap, {
Key? key,
this.duration = const Duration(milliseconds: 200),
this.curve = Curves.easeInOut,
}) : super(key: key);
final Duration duration;
final double gap;
final Curve curve;
@override
State<AnimatedSliverGap> createState() => _AnimatedSliverGapState();
}
class _AnimatedSliverGapState extends State<AnimatedSliverGap> with SingleTickerProviderStateMixin {
late final _controller = AnimationController(
vsync: this,
value: widget.gap,
upperBound: double.infinity,
);
@override
void didUpdateWidget(covariant AnimatedSliverGap oldWidget) {
if (oldWidget.gap != widget.gap) {
_controller.animateTo(
widget.gap,
curve: widget.curve,
duration: widget.duration,
);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<double>(
valueListenable: _controller,
builder: (context, gap, _) {
return SliverGap(gap);
},
);
}
}
class SliverGap extends LeafRenderObjectWidget {
const SliverGap(this.gap, {Key? key}) : super(key: key);
final double gap;
static const size4 = SliverGap(4);
static const size8 = SliverGap(8);
static const size16 = SliverGap(16);
static const size24 = SliverGap(24);
static const size32 = SliverGap(32);
static const size64 = SliverGap(64);
@override
RenderSliverGap createRenderObject(BuildContext context) => RenderSliverGap(gap);
@override
void updateRenderObject(BuildContext context, covariant RenderSliverGap renderObject) {
renderObject.gap = gap;
super.updateRenderObject(context, renderObject);
}
}
class RenderSliverGap extends RenderSliver {
RenderSliverGap(this._gap) : super() {
markNeedsLayout();
}
double _gap;
double get gap => _gap;
set gap(double value) {
if (_gap != value) {
_gap = value;
markNeedsLayout();
}
}
@override
void performLayout() {
final cacheExtent = calculateCacheOffset(constraints, from: 0, to: gap);
final paintExtent = calculatePaintOffset(constraints, from: 0, to: gap);
geometry = SliverGeometry(
paintExtent: paintExtent,
scrollExtent: gap,
visible: false,
cacheExtent: cacheExtent,
maxPaintExtent: gap,
);
}
}

View file

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
class GalleryView extends StatelessWidget {
const GalleryView({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}