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

23
lib/app.dart Normal file
View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'features/core/abstracts/app_setup.dart';
import 'features/core/abstracts/router/app_router.dart';
import 'features/core/data/constants/const_colors.dart';
class McgApp extends StatelessWidget {
const McgApp({super.key});
@override
Widget build(BuildContext context) {
final appRouter = McgRouter.locate.router;
return MaterialApp.router(
title: 'MC Gallery App',
theme: ConstThemes.materialLightTheme,
darkTheme: ConstThemes.materialDarkTheme,
supportedLocales: AppSetup.supportedLocales,
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
routeInformationProvider: appRouter.routeInformationProvider,
);
}
}

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

View file

@ -0,0 +1,63 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that looks up messages for specific locales by
// delegating to the appropriate library.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:implementation_imports, file_names, unnecessary_new
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
// ignore_for_file:argument_type_not_assignable, invalid_assignment
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
// ignore_for_file:comment_references
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
import 'package:intl/src/intl_helpers.dart';
import 'messages_en.dart' as messages_en;
typedef Future<dynamic> LibraryLoader();
Map<String, LibraryLoader> _deferredLibraries = {
'en': () => new SynchronousFuture(null),
};
MessageLookupByLibrary? _findExact(String localeName) {
switch (localeName) {
case 'en':
return messages_en.messages;
default:
return null;
}
}
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) {
var availableLocale = Intl.verifiedLocale(
localeName, (locale) => _deferredLibraries[locale] != null,
onFailure: (_) => null);
if (availableLocale == null) {
return new SynchronousFuture(false);
}
var lib = _deferredLibraries[availableLocale];
lib == null ? new SynchronousFuture(false) : lib();
initializeInternalMessageLookup(() => new CompositeMessageLookup());
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
return new SynchronousFuture(true);
}
bool _messagesExistFor(String locale) {
try {
return _findExact(locale) != null;
} catch (e) {
return false;
}
}
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
var actualLocale =
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
if (actualLocale == null) return null;
return _findExact(actualLocale);
}

View file

@ -0,0 +1,28 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a en locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';
final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"somethingWentWrong":
MessageLookupByLibrary.simpleMessage("Something went wrong")
};
}

View file

@ -0,0 +1,88 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'intl/messages_all.dart';
// **************************************************************************
// Generator: Flutter Intl IDE plugin
// Made by Localizely
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
class Strings {
Strings();
static Strings? _current;
static Strings get current {
assert(_current != null,
'No instance of Strings was loaded. Try to initialize the Strings delegate before accessing Strings.current.');
return _current!;
}
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<Strings> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false)
? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
final instance = Strings();
Strings._current = instance;
return instance;
});
}
static Strings of(BuildContext context) {
final instance = Strings.maybeOf(context);
assert(instance != null,
'No instance of Strings present in the widget tree. Did you add Strings.delegate in localizationsDelegates?');
return instance!;
}
static Strings? maybeOf(BuildContext context) {
return Localizations.of<Strings>(context, Strings);
}
/// `Something went wrong`
String get somethingWentWrong {
return Intl.message(
'Something went wrong',
name: 'somethingWentWrong',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<Strings> {
const AppLocalizationDelegate();
List<Locale> get supportedLocales {
return const <Locale>[
Locale.fromSubtags(languageCode: 'en'),
];
}
@override
bool isSupported(Locale locale) => _isSupported(locale);
@override
Future<Strings> load(Locale locale) => Strings.load(locale);
@override
bool shouldReload(AppLocalizationDelegate old) => false;
bool _isSupported(Locale locale) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode) {
return true;
}
}
return false;
}
}

4
lib/l10n/intl_en.arb Normal file
View file

@ -0,0 +1,4 @@
{
"@@locale": "en",
"somethingWentWrong": "Something went wrong"
}

48
lib/locator.dart Normal file
View file

@ -0,0 +1,48 @@
import 'package:get_it/get_it.dart';
import 'package:mc_gallery/features/core/abstracts/router/app_router.dart';
import 'package:mc_gallery/features/core/services/logging_service.dart';
import 'package:mc_gallery/features/core/services/navigation_service.dart';
GetIt get locate => Locator.instance();
class Locator {
static GetIt instance() => GetIt.instance;
static T locate<T extends Object>() => instance().get<T>();
static Future<void> setup() async {
final locator = instance();
_registerAPIs();
_registerViewModels();
_registerLazySingletons();
_registerFactories();
await _registerServices(locator);
await _registerRepos(locator);
_registerSingletons();
}
static void _registerAPIs() {}
static void _registerViewModels() {}
static void _registerLazySingletons() {}
static void _registerFactories() {}
static _registerServices(GetIt it) {
it.registerLazySingleton(
() => NavigationService(
mcgRouter: McgRouter.locate,
),
);
it.registerFactory(
() => LoggingService(),
);
}
static _registerRepos(GetIt locator) {}
static void _registerSingletons() {}
}

16
lib/main.dart Normal file
View file

@ -0,0 +1,16 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'app.dart';
import 'features/core/abstracts/app_setup.dart';
Future<void> main() async {
await runZonedGuarded(
() async {
await AppSetup.initialise();
runApp(const McgApp());
},
AppSetup.onUncaughtException,
);
}