ui backbone
This commit is contained in:
parent
3e374d24f6
commit
49c97192c2
24 changed files with 917 additions and 72 deletions
|
@ -1,29 +1,218 @@
|
||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
analyzer:
|
||||||
# check for errors, warnings, and lints.
|
strong-mode:
|
||||||
#
|
implicit-dynamic: true
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
implicit-casts: true
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
errors:
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
avoid_dynamic_calls: error
|
||||||
include: package:flutter_lints/flutter.yaml
|
missing_required_param: error
|
||||||
|
missing_return: error
|
||||||
|
# allow having TODOs in the code
|
||||||
|
todo: info
|
||||||
|
missing_enum_constant_in_switch: error
|
||||||
|
implicit_dynamic_type: info
|
||||||
|
implicit_dynamic_map_literal: info
|
||||||
|
implicit_dynamic_list_literal: info
|
||||||
|
implicit_dynamic_method: warning
|
||||||
|
implicit_dynamic_function: warning
|
||||||
|
invalid_use_of_protected_member: error
|
||||||
|
unused_local_variable: warning
|
||||||
|
deprecated_member_use: warning
|
||||||
|
unused_element: warning
|
||||||
|
unused_field: warning
|
||||||
|
dead_code: error
|
||||||
|
must_call_super: error
|
||||||
|
|
||||||
|
# linter errors
|
||||||
|
unnecessary_statements: error
|
||||||
|
recursive_getters: error
|
||||||
|
unnecessary_new: error
|
||||||
|
unnecessary_getters_setters: error
|
||||||
|
use_function_type_syntax_for_parameters: error
|
||||||
|
no_duplicate_case_values: error
|
||||||
|
no_adjacent_strings_in_list: error
|
||||||
|
non_constant_identifier_names: error
|
||||||
|
constant_identifier_names: error
|
||||||
|
avoid_returning_this: error
|
||||||
|
prefer_const_constructors_in_immutables: error
|
||||||
|
prefer_const_literals_to_create_immutables: error
|
||||||
|
avoid_equals_and_hash_code_on_mutable_classes: error
|
||||||
|
camel_case_types: error
|
||||||
|
camel_case_extensions: error
|
||||||
|
library_names: error
|
||||||
|
file_names: error
|
||||||
|
library_prefixes: error
|
||||||
|
|
||||||
|
# linter warnings
|
||||||
|
avoid_print: warning
|
||||||
|
unnecessary_lambdas: warning
|
||||||
|
use_key_in_widget_constructors: warning
|
||||||
|
prefer_final_fields: warning
|
||||||
|
prefer_final_locals: warning
|
||||||
|
prefer_final_in_for_each: warning
|
||||||
|
prefer_const_constructors: warning
|
||||||
|
unnecessary_const: warning
|
||||||
|
unnecessary_brace_in_string_interps: warning
|
||||||
|
|
||||||
|
# Ignore analyzer hints for updating pubspecs when using Future or
|
||||||
|
# Stream and not importing dart:async
|
||||||
|
# Please see https://github.com/flutter/flutter/pull/24528 for details.
|
||||||
|
sdk_version_async_exported_from_core: ignore
|
||||||
|
exclude:
|
||||||
|
- "bin/cache/**"
|
||||||
|
- "**/*.chopper.dart"
|
||||||
|
- "**/generated/**"
|
||||||
|
- "**/*.g.dart"
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at
|
|
||||||
# https://dart-lang.github.io/linter/lints/index.html.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# these rules are documented on and in the same order as
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# the Dart Lint rules page to make maintenance easier
|
||||||
|
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
|
||||||
# Additional information about this file can be found at
|
- always_declare_return_types
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# always_put_control_body_on_new_line
|
||||||
|
# always_put_required_named_parameters_first
|
||||||
|
- always_require_non_null_named_parameters
|
||||||
|
- avoid_dynamic_calls
|
||||||
|
# - always_specify_types
|
||||||
|
- annotate_overrides
|
||||||
|
- avoid_print
|
||||||
|
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
|
||||||
|
- avoid_returning_this
|
||||||
|
- avoid_init_to_null
|
||||||
|
- avoid_bool_literals_in_conditional_expressions
|
||||||
|
# - avoid_catches_without_on_clauses # we do this commonly
|
||||||
|
# - avoid_catching_errors # we do this commonly
|
||||||
|
# - avoid_classes_with_only_static_members
|
||||||
|
# - avoid_double_and_int_checks # only useful when targeting JS runtime
|
||||||
|
- avoid_empty_else
|
||||||
|
- avoid_field_initializers_in_const_classes
|
||||||
|
- avoid_function_literals_in_foreach_calls
|
||||||
|
# - avoid_js_rounded_ints # only useful when targeting JS runtime
|
||||||
|
- avoid_null_checks_in_equality_operators
|
||||||
|
- avoid_positional_boolean_parameters
|
||||||
|
# - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
|
||||||
|
- avoid_relative_lib_imports
|
||||||
|
- avoid_renaming_method_parameters
|
||||||
|
- avoid_return_types_on_setters
|
||||||
|
# - avoid_returning_null # there are plenty of valid reasons to return null
|
||||||
|
# - avoid_returning_null_for_future # not yet tested
|
||||||
|
- avoid_returning_null_for_void
|
||||||
|
# - avoid_returning_this # there are plenty of valid reasons to return this
|
||||||
|
# - avoid_setters_without_getters # not yet tested
|
||||||
|
# - avoid_shadowing_type_parameters # not yet tested
|
||||||
|
# - avoid_single_cascade_in_expression_statements # not yet tested
|
||||||
|
- avoid_slow_async_io
|
||||||
|
- avoid_types_as_parameter_names
|
||||||
|
# - avoid_types_on_closure_parameters # conflicts with always_specify_types
|
||||||
|
- avoid_unused_constructor_parameters
|
||||||
|
- avoid_void_async
|
||||||
|
- await_only_futures
|
||||||
|
- camel_case_types
|
||||||
|
- cancel_subscriptions
|
||||||
|
# - cascade_invocations # not yet tested
|
||||||
|
# - close_sinks # not reliable enough
|
||||||
|
# - comment_references # blocked on https://github.com/flutter/flutter/issues/20765
|
||||||
|
# - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
|
||||||
|
- control_flow_in_finally
|
||||||
|
# - curly_braces_in_flow_control_structures # not yet tested
|
||||||
|
# - diagnostic_describe_all_properties # not yet tested
|
||||||
|
- directives_ordering
|
||||||
|
- empty_catches
|
||||||
|
- empty_constructor_bodies
|
||||||
|
- empty_statements
|
||||||
|
# - file_names # not yet tested
|
||||||
|
- hash_and_equals
|
||||||
|
- implementation_imports
|
||||||
|
# - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
|
||||||
|
- iterable_contains_unrelated_type
|
||||||
|
# - join_return_with_assignment # not yet tested
|
||||||
|
- library_names
|
||||||
|
- library_prefixes
|
||||||
|
# - lines_longer_than_80_chars # not yet tested
|
||||||
|
- list_remove_unrelated_type
|
||||||
|
# - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
|
||||||
|
- no_adjacent_strings_in_list
|
||||||
|
- no_duplicate_case_values
|
||||||
|
- non_constant_identifier_names
|
||||||
|
# - null_closures # not yet tested
|
||||||
|
# - omit_local_variable_types # opposite of always_specify_types
|
||||||
|
# - one_member_abstracts # too many false positives
|
||||||
|
# - only_throw_errors # https://github.com/flutter/flutter/issues/5792
|
||||||
|
- overridden_fields
|
||||||
|
- package_api_docs
|
||||||
|
- package_names
|
||||||
|
- package_prefixed_library_names
|
||||||
|
# - parameter_assignments # we do this commonly
|
||||||
|
- prefer_adjacent_string_concatenation
|
||||||
|
- prefer_asserts_in_initializer_lists
|
||||||
|
# - prefer_asserts_with_message # not yet tested
|
||||||
|
- prefer_collection_literals
|
||||||
|
- prefer_conditional_assignment
|
||||||
|
- prefer_const_constructors
|
||||||
|
- prefer_const_constructors_in_immutables
|
||||||
|
- prefer_const_declarations
|
||||||
|
- prefer_const_literals_to_create_immutables
|
||||||
|
# - prefer_constructors_over_static_methods # not yet tested
|
||||||
|
- prefer_contains
|
||||||
|
# - prefer_double_quotes # opposite of prefer_single_quotes
|
||||||
|
- prefer_equal_for_default_values
|
||||||
|
# - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
|
||||||
|
- prefer_final_fields
|
||||||
|
# - prefer_final_in_for_each # not yet tested
|
||||||
|
- prefer_final_locals
|
||||||
|
# - prefer_for_elements_to_map_fromIterable # not yet tested
|
||||||
|
- prefer_foreach
|
||||||
|
# - prefer_function_declarations_over_variables # not yet tested
|
||||||
|
- prefer_generic_function_type_aliases
|
||||||
|
# - prefer_if_elements_to_conditional_expressions # not yet tested
|
||||||
|
- prefer_if_null_operators
|
||||||
|
- prefer_initializing_formals
|
||||||
|
- prefer_inlined_adds
|
||||||
|
# - prefer_int_literals # not yet tested
|
||||||
|
# - prefer_interpolation_to_compose_strings # not yet tested
|
||||||
|
- prefer_is_empty
|
||||||
|
- prefer_is_not_empty
|
||||||
|
- prefer_iterable_whereType
|
||||||
|
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
|
||||||
|
# - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932
|
||||||
|
- prefer_single_quotes
|
||||||
|
- prefer_spread_collections
|
||||||
|
- prefer_typing_uninitialized_variables
|
||||||
|
- prefer_void_to_null
|
||||||
|
# - provide_deprecation_message # not yet tested
|
||||||
|
# - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml
|
||||||
|
- recursive_getters
|
||||||
|
- slash_for_doc_comments
|
||||||
|
# - sort_child_properties_last # not yet tested
|
||||||
|
# - sort_constructors_first
|
||||||
|
- sort_pub_dependencies
|
||||||
|
- sort_unnamed_constructors_first
|
||||||
|
- test_types_in_equals
|
||||||
|
- throw_in_finally
|
||||||
|
# - type_annotate_public_apis # subset of always_specify_types
|
||||||
|
- type_init_formals
|
||||||
|
- unawaited_futures
|
||||||
|
# - unnecessary_await_in_return # not yet tested
|
||||||
|
- unnecessary_brace_in_string_interps
|
||||||
|
- unnecessary_const
|
||||||
|
- unnecessary_getters_setters
|
||||||
|
# - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
|
||||||
|
- unnecessary_new
|
||||||
|
- unnecessary_null_aware_assignments
|
||||||
|
- unnecessary_null_in_if_null_operators
|
||||||
|
- unnecessary_overrides
|
||||||
|
- unnecessary_parenthesis
|
||||||
|
- unnecessary_statements
|
||||||
|
- unnecessary_this
|
||||||
|
- unrelated_type_equality_checks
|
||||||
|
# - unsafe_html # not yet tested
|
||||||
|
- use_full_hex_values_for_flutter_colors
|
||||||
|
# - use_function_type_syntax_for_parameters # not yet tested
|
||||||
|
- use_rethrow_when_possible
|
||||||
|
- use_key_in_widget_constructors
|
||||||
|
# - use_setters_to_change_properties # not yet tested
|
||||||
|
# - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
|
||||||
|
# - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
|
||||||
|
- valid_regexps
|
||||||
|
# - void_checks # not yet tested
|
||||||
|
|
|
@ -12,6 +12,8 @@ abstract class AppSetup {
|
||||||
static Future<void> initialise() async {
|
static Future<void> initialise() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
await _setupStrings();
|
||||||
|
|
||||||
await SystemChrome.setPreferredOrientations([
|
await SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.portraitUp,
|
DeviceOrientation.portraitUp,
|
||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
|
@ -38,7 +40,7 @@ abstract class AppSetup {
|
||||||
return supportedLocales.first;
|
return supportedLocales.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setupStrings() async {
|
static Future<void> _setupStrings() async {
|
||||||
await Strings.load(resolveLocale(WidgetsBinding.instance.window.locales, supportedLocales));
|
await Strings.load(resolveLocale(WidgetsBinding.instance.window.locales, supportedLocales));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.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 '../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);
|
final ValueNotifier<bool> _isInitialised = ValueNotifier(false);
|
||||||
ValueListenable<bool> get isInitialised => _isInitialised;
|
ValueListenable<bool> get isInitialised => _isInitialised;
|
||||||
|
|
||||||
|
@ -23,12 +23,11 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
|
||||||
String get errorMessage => _errorMessage ?? strings.somethingWentWrong;
|
String get errorMessage => _errorMessage ?? strings.somethingWentWrong;
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void initialise(DisposableBuildContext disposableBuildContext, bool Function() mounted,
|
void initialise(bool Function() mounted, [T? arguments]) {
|
||||||
[E? arguments]) {
|
|
||||||
_mounted = mounted;
|
_mounted = mounted;
|
||||||
_isInitialised.value = true;
|
_isInitialised.value = true;
|
||||||
_state.value = ViewModelState.isInitialised;
|
_state.value = ViewModelState.isInitialised;
|
||||||
_loggingService.successfulInit(location: runtimeType.runtimeType.toString());
|
_loggingService.successfulInit(location: runtimeType.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBusy(bool isBusy) {
|
void setBusy(bool isBusy) {
|
||||||
|
@ -56,7 +55,7 @@ abstract class BaseViewModel<E extends Object?> extends ChangeNotifier {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_loggingService.successfulDispose(location: runtimeType.runtimeType.toString());
|
_loggingService.successfulDispose(location: runtimeType.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
late final bool Function() _mounted;
|
late final bool Function() _mounted;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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/gallery/gallery_view.dart';
|
||||||
|
import 'package:mc_gallery/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';
|
||||||
|
@ -20,6 +21,15 @@ class McgRouter {
|
||||||
path: Routes.home.routePath,
|
path: Routes.home.routePath,
|
||||||
name: Routes.home.routeName,
|
name: Routes.home.routeName,
|
||||||
builder: (context, _) => const GalleryView(),
|
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',
|
routeName: 'Home',
|
||||||
)),
|
)),
|
||||||
imageCarousel(RoutesInfo(
|
imageCarousel(RoutesInfo(
|
||||||
routePath: '/image_carousel',
|
routePath: 'image_carousel',
|
||||||
routeName: 'Image Carousel',
|
routeName: 'Image Carousel',
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,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 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 white = Colors.white;
|
||||||
static const black = Colors.black;
|
static const black = Colors.black;
|
||||||
|
|
|
@ -4,6 +4,6 @@ abstract class ConstValues {
|
||||||
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 = 20;
|
||||||
static const int minImageSize = 100;
|
static const int minImageSize = 50;
|
||||||
static const int maxImageSize = 250;
|
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: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 '../abstracts/router/app_router.dart';
|
import '../abstracts/router/app_router.dart';
|
||||||
import '../abstracts/router/routes.dart';
|
import '../abstracts/router/routes.dart';
|
||||||
|
@ -11,11 +13,19 @@ class NavigationService {
|
||||||
|
|
||||||
final McgRouter _mcgRouter;
|
final McgRouter _mcgRouter;
|
||||||
|
|
||||||
void pushImageCarouselView(BuildContext context) =>
|
void pushImageCarouselView(
|
||||||
context.pushNamed(Routes.imageCarousel.routeName);
|
BuildContext context, {
|
||||||
|
required ImageCarouselViewArguments imageCarouselViewArguments,
|
||||||
|
}) =>
|
||||||
|
context.pushNamed(
|
||||||
|
Routes.imageCarousel.routeName,
|
||||||
|
extra: imageCarouselViewArguments,
|
||||||
|
);
|
||||||
|
|
||||||
void backToGallery(BuildContext context) => context.pop();
|
void backToGallery(BuildContext context) => context.pop();
|
||||||
|
|
||||||
void previous() {}
|
void previous() {}
|
||||||
void next() {}
|
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/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ import '../services/connection_service.dart';
|
||||||
class McgScaffold extends StatelessWidget {
|
class McgScaffold extends StatelessWidget {
|
||||||
const McgScaffold({
|
const McgScaffold({
|
||||||
this.appBar,
|
this.appBar,
|
||||||
this.bodyBuilderCompleter,
|
this.bodyBuilderWaiter,
|
||||||
this.body,
|
this.body,
|
||||||
this.waitingWidget,
|
this.waitingWidget,
|
||||||
this.forceInternetCheck = false,
|
this.forceInternetCheck = false,
|
||||||
|
@ -19,10 +18,10 @@ class McgScaffold extends StatelessWidget {
|
||||||
final AppBar? appBar;
|
final AppBar? appBar;
|
||||||
|
|
||||||
/// Awaits an external signal (complete) before building the body.
|
/// Awaits an external signal (complete) before building the body.
|
||||||
final Completer? bodyBuilderCompleter;
|
final ValueListenable<bool>? bodyBuilderWaiter;
|
||||||
final Widget? body;
|
final Widget? body;
|
||||||
|
|
||||||
/// Custom widget to be used while awaiting [bodyBuilderCompleter].
|
/// Custom widget to be used while awaiting [bodyBuilderWaiter].
|
||||||
///
|
///
|
||||||
/// Defaults to using [PlatformCircularProgressIndicator].
|
/// Defaults to using [PlatformCircularProgressIndicator].
|
||||||
final Widget? waitingWidget;
|
final Widget? waitingWidget;
|
||||||
|
@ -32,19 +31,12 @@ class McgScaffold extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final Widget loginOptionsBody = bodyBuilderCompleter != null
|
final Widget loginOptionsBody = bodyBuilderWaiter != null
|
||||||
? FutureBuilder(
|
? ValueListenableBuilder<bool>(
|
||||||
future: bodyBuilderCompleter!.future,
|
valueListenable: bodyBuilderWaiter!,
|
||||||
builder: (context, snapshot) {
|
builder: (context, final isReady, child) => !isReady
|
||||||
switch (snapshot.connectionState) {
|
? Center(child: waitingWidget ?? const CircularProgressIndicator())
|
||||||
case ConnectionState.none:
|
: body ?? const SizedBox.shrink(),
|
||||||
case ConnectionState.waiting:
|
|
||||||
case ConnectionState.active:
|
|
||||||
return Center(child: waitingWidget ?? const CircularProgressIndicator());
|
|
||||||
case ConnectionState.done:
|
|
||||||
return body ?? const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
: 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);
|
final imageUri = _imageUrlGenerator(imageSide: imageSide);
|
||||||
|
|
||||||
return ImageModel<int>(
|
return ImageModel(
|
||||||
comparableIndex: imageIndex,
|
imageIndex: imageIndex,
|
||||||
uri: imageUri,
|
uri: imageUri,
|
||||||
imageName: Strings.current.image,
|
imageName: Strings.current.image,
|
||||||
);
|
);
|
||||||
|
@ -29,6 +29,6 @@ class UnsplashImagesApi implements ImagesApi {
|
||||||
Uri _imageUrlGenerator({required int imageSide}) => Uri(
|
Uri _imageUrlGenerator({required int imageSide}) => Uri(
|
||||||
scheme: ConstValues.httpsScheme,
|
scheme: ConstValues.httpsScheme,
|
||||||
host: ConstValues.backendHost,
|
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({
|
const ImageModel({
|
||||||
required this.uri,
|
required this.uri,
|
||||||
required this.comparableIndex,
|
required this.imageIndex,
|
||||||
required this.imageName,
|
required this.imageName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class ImageModel<T extends Comparable> {
|
||||||
final Uri uri;
|
final Uri uri;
|
||||||
|
|
||||||
/// A unique identifier that can be used for indexing the image.
|
/// A unique identifier that can be used for indexing the image.
|
||||||
final T comparableIndex;
|
final int imageIndex;
|
||||||
|
|
||||||
/// Given name of the image.
|
/// Given name of the image.
|
||||||
final String imageName;
|
final String imageName;
|
||||||
|
|
|
@ -15,6 +15,7 @@ class ImagesService {
|
||||||
final ImagesApi _imagesApi;
|
final ImagesApi _imagesApi;
|
||||||
|
|
||||||
late final Iterable<ImageModel> _imageModels;
|
late final Iterable<ImageModel> _imageModels;
|
||||||
|
Iterable<ImageModel> get imageModels => _imageModels;
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_imageModels = await _imagesApi.fetchImageUri(token: '');
|
_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 {
|
class GalleryView extends StatelessWidget {
|
||||||
const GalleryView({super.key});
|
const GalleryView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
|
"gallery": MessageLookupByLibrary.simpleMessage("Gallery"),
|
||||||
"image": MessageLookupByLibrary.simpleMessage("Image"),
|
"image": MessageLookupByLibrary.simpleMessage("Image"),
|
||||||
|
"imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"),
|
||||||
"somethingWentWrong":
|
"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: [],
|
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> {
|
class AppLocalizationDelegate extends LocalizationsDelegate<Strings> {
|
||||||
|
|
|
@ -2,5 +2,10 @@
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
"somethingWentWrong": "Something went wrong",
|
"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:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:internet_connection_checker/internet_connection_checker.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/core/services/navigation_service.dart';
|
||||||
import 'package:mc_gallery/features/home/api/unsplash_images_api.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/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/connection_service.dart';
|
||||||
import 'features/core/services/overlay_service.dart';
|
import 'features/core/services/overlay_service.dart';
|
||||||
|
|
||||||
|
@ -23,8 +28,8 @@ class Locator {
|
||||||
_registerViewModels();
|
_registerViewModels();
|
||||||
|
|
||||||
await _registerServices(locator);
|
await _registerServices(locator);
|
||||||
|
|
||||||
await _registerRepos(locator);
|
await _registerRepos(locator);
|
||||||
|
|
||||||
_registerSingletons();
|
_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(
|
it.registerLazySingleton(
|
||||||
() => NavigationService(
|
() => NavigationService(
|
||||||
mcgRouter: McgRouter.locate,
|
mcgRouter: McgRouter.locate,
|
||||||
|
@ -57,6 +79,12 @@ class Locator {
|
||||||
),
|
),
|
||||||
dispose: (param) => param.dispose(),
|
dispose: (param) => param.dispose(),
|
||||||
);
|
);
|
||||||
|
instance().registerSingleton<AppLifecycleService>(
|
||||||
|
AppLifecycleService(
|
||||||
|
loggingService: LoggingService.locate,
|
||||||
|
),
|
||||||
|
dispose: (param) async => await param.dispose(),
|
||||||
|
);
|
||||||
it.registerSingleton(
|
it.registerSingleton(
|
||||||
ImagesService(
|
ImagesService(
|
||||||
imagesApi: UnsplashImagesApi(),
|
imagesApi: UnsplashImagesApi(),
|
||||||
|
@ -65,7 +93,7 @@ class Locator {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static _registerRepos(GetIt locator) {}
|
static FutureOr<void> _registerRepos(GetIt locator) {}
|
||||||
|
|
||||||
static void _registerSingletons() {}
|
static void _registerSingletons() {}
|
||||||
}
|
}
|
||||||
|
|
175
pubspec.lock
175
pubspec.lock
|
@ -50,6 +50,27 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
cached_network_image:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: cached_network_image
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.3"
|
||||||
|
cached_network_image_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
cached_network_image_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cached_network_image_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -153,6 +174,20 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_blurhash:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_blurhash
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.0"
|
||||||
|
flutter_cache_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_cache_manager
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -275,6 +310,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.0"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
nm:
|
nm:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -282,6 +324,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
|
octo_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: octo_image
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -310,6 +359,62 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.22"
|
||||||
|
path_provider_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.7"
|
||||||
|
path_provider_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_macos
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.5"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.3"
|
||||||
|
pedantic:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pedantic
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.11.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -317,6 +422,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.1.0"
|
||||||
|
platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -331,6 +443,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.6.2"
|
version: "3.6.2"
|
||||||
|
process:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: process
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.2.4"
|
||||||
|
provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.5"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -338,6 +464,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.27.7"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -350,6 +483,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.2"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0+2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -371,6 +518,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0+3"
|
||||||
talker:
|
talker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -413,6 +567,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -427,6 +588,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0+2"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -16,11 +16,14 @@ dependencies:
|
||||||
# Routing
|
# Routing
|
||||||
go_router: ^6.0.0
|
go_router: ^6.0.0
|
||||||
|
|
||||||
# Service locator
|
# State management
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
|
provider: ^6.0.5
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
dio: ^4.0.6
|
dio: ^4.0.6
|
||||||
|
cached_network_image: ^3.2.3
|
||||||
|
flutter_cache_manager: ^3.3.0
|
||||||
|
|
||||||
# Util backend
|
# Util backend
|
||||||
intl_utils: ^2.8.1
|
intl_utils: ^2.8.1
|
||||||
|
|
Loading…
Reference in a new issue