diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/features/core/data/constants/const_durations.dart b/lib/features/core/data/constants/const_durations.dart new file mode 100644 index 0000000..a6d446a --- /dev/null +++ b/lib/features/core/data/constants/const_durations.dart @@ -0,0 +1,9 @@ +abstract class ConstDurations { + static const Duration tripleDefaultAnimationDuration = Duration(milliseconds: 1200); + static const Duration doubleDefaultAnimationDuration = Duration(milliseconds: 800); + static const Duration oneAndHalfDefaultAnimationDuration = Duration(milliseconds: 600); + static const Duration defaultAnimationDuration = Duration(milliseconds: 400); + static const Duration halfDefaultAnimationDuration = Duration(milliseconds: 200); + static const Duration quarterDefaultAnimationDuration = Duration(milliseconds: 100); + static const Duration zero = Duration.zero; +} diff --git a/lib/features/core/data/constants/const_input_formatters.dart b/lib/features/core/data/constants/const_input_formatters.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/core/data/constants/const_input_validators.dart b/lib/features/core/data/constants/const_input_validators.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/core/services/connection_service.dart b/lib/features/core/services/connection_service.dart index e69de29..97ddd14 100644 --- a/lib/features/core/services/connection_service.dart +++ b/lib/features/core/services/connection_service.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; + +import '/locator.dart'; + +/// Used to observe the current connection type. +class ConnectionService { + ConnectionService({ + required Connectivity connectivity, + required InternetConnectionChecker internetConnectionChecker, + bool shouldInitialize = true, + }) : _connectivity = connectivity, + _internetConnectionChecker = internetConnectionChecker { + if (shouldInitialize) initialize(); + } + + ConnectivityResult? _connectivityResult; + ConnectivityResult? get connectivityResult => _connectivityResult; + + final Connectivity _connectivity; + final Map _connectivitySubscriptions = {}; + + final InternetConnectionChecker _internetConnectionChecker; + + final Completer _isInitialized = Completer(); + + Completer? _hasInternetConnection; + + final ValueNotifier _hasInternetConnectionListenable = ValueNotifier(false); + ValueListenable get hasInternetConnectionListenable => _hasInternetConnectionListenable; + + Future get hasInternetConnection async { + try { + if (_hasInternetConnection == null) { + _hasInternetConnection = Completer(); + try { + final hasInternetConnection = await _internetConnectionChecker.hasConnection; + _hasInternetConnection!.complete(hasInternetConnection); + return hasInternetConnection; + } catch (error) { + _hasInternetConnection!.complete(false); + return false; + } finally { + _hasInternetConnection = null; + } + } else { + final awaitedHasInternet = await _hasInternetConnection!.future; + + return awaitedHasInternet; + } + } on SocketException catch (_, __) { + return false; + } + } + + Future initialize() async { + try { + final tag = runtimeType.toString(); + if (_connectivitySubscriptions[tag] != null) { + await dispose(); + } + _connectivitySubscriptions[tag] = _connectivity.onConnectivityChanged.listen( + _onConnectivityChanged, + cancelOnError: false, + onError: (error, stack) {}, + ); + await _onConnectivityChanged(await _connectivity.checkConnectivity()); + _internetConnectionChecker.onStatusChange.listen((final event) { + switch (event) { + case InternetConnectionStatus.connected: + _hasInternetConnectionListenable.value = true; + break; + case InternetConnectionStatus.disconnected: + _hasInternetConnectionListenable.value = false; + break; + } + }); + _isInitialized.complete(); + } catch (error, stackTrace) {} + } + + Future dispose() async { + for (final subscription in _connectivitySubscriptions.values) { + await subscription.cancel(); + } + _connectivitySubscriptions.clear(); + _connectivityResult = null; + } + + Future addListener({ + required String tag, + required Future Function({ + required ConnectivityResult connectivityResult, + required bool hasInternet, + }) + listener, + bool tryCallListenerOnAdd = true, + }) async { + try { + if (_connectivityResult != null && tryCallListenerOnAdd) + await listener( + connectivityResult: _connectivityResult!, hasInternet: await hasInternetConnection); + _connectivitySubscriptions[tag] = + _connectivity.onConnectivityChanged.listen((connectivityResult) async { + await listener( + connectivityResult: connectivityResult, hasInternet: await hasInternetConnection); + }); + } catch (error, stackTrace) {} + } + + Future removeListener({required String tag}) async { + try { + final subscription = _connectivitySubscriptions[tag]; + if (subscription != null) { + await subscription.cancel(); + } else {} + } catch (error, stackTrace) {} + } + + Future _onConnectivityChanged(ConnectivityResult connectivityResult) async { + try { + _connectivityResult = connectivityResult; + } catch (error, stackTrace) {} + } + + static ConnectionService get locate => Locator.locate(); +} + +class NoInternetException implements Exception {} diff --git a/lib/features/core/services/overlay_service.dart b/lib/features/core/services/overlay_service.dart new file mode 100644 index 0000000..3ebc266 --- /dev/null +++ b/lib/features/core/services/overlay_service.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:mc_gallery/features/core/services/logging_service.dart'; +import 'package:mc_gallery/locator.dart'; + +class OverlayService { + const OverlayService({ + required LoggingService loggingService, + }) : _loggingService = loggingService; + + final LoggingService _loggingService; + + final Map _animationControllerMap = const {}; + final Map _overlayEntryMap = const {}; + + Future playOverlayEntry({ + required BuildContext context, + required AnimationController animationController, + required OverlayEntry overlayEntry, + }) async { + try { + _animationControllerMap[animationController.hashCode] = animationController; + _overlayEntryMap[overlayEntry.hashCode] = overlayEntry; + Overlay.of( + context, + rootOverlay: true, + )! + .insert(overlayEntry); + + await animationController.forward(); + if (overlayEntry.mounted) overlayEntry.remove(); + + _overlayEntryMap.remove(overlayEntry.hashCode); + animationController.dispose(); + _animationControllerMap.remove(animationController.hashCode); + } catch (error, stackTrace) { + _loggingService.handle(error, stackTrace); + } + } + + void dispose() { + for (final overlayEntry in _overlayEntryMap.values) { + if (overlayEntry.mounted) { + overlayEntry.remove(); + } + } + _overlayEntryMap.clear(); + for (final animationController in _animationControllerMap.values) { + if (animationController.isAnimating) { + animationController.dispose(); + } + } + _animationControllerMap.clear(); + } + + static OverlayService get locate => Locator.locate(); +} diff --git a/lib/features/core/widgets/mcg_scaffold.dart b/lib/features/core/widgets/mcg_scaffold.dart new file mode 100644 index 0000000..677c168 --- /dev/null +++ b/lib/features/core/widgets/mcg_scaffold.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../data/constants/const_durations.dart'; +import '../services/connection_service.dart'; + +class McgScaffold extends StatelessWidget { + const McgScaffold({ + this.appBar, + this.bodyBuilderCompleter, + this.body, + this.waitingWidget, + this.forceInternetCheck = false, + super.key, + }); + + final AppBar? appBar; + + /// Awaits an external signal (complete) before building the body. + final Completer? bodyBuilderCompleter; + final Widget? body; + + /// Custom widget to be used while awaiting [bodyBuilderCompleter]. + /// + /// Defaults to using [PlatformCircularProgressIndicator]. + final Widget? waitingWidget; + + /// Enabling listing to [ConnectionState], showing a small text at the top, when connectivity is lost. + final bool forceInternetCheck; + + @override + Widget build(BuildContext context) { + final Widget loginOptionsBody = bodyBuilderCompleter != null + ? FutureBuilder( + future: bodyBuilderCompleter!.future, + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + return Center(child: waitingWidget ?? const CircularProgressIndicator()); + case ConnectionState.done: + return body ?? const SizedBox.shrink(); + } + }, + ) + : body ?? const SizedBox.shrink(); + + return Scaffold( + appBar: appBar, + body: forceInternetCheck + ? SingleChildScrollView( + child: ValueListenableBuilder( + valueListenable: ConnectionService.locate.hasInternetConnectionListenable, + builder: (context, hasInternetConnection, _) => Column( + children: [ + AnimatedSwitcher( + duration: ConstDurations.defaultAnimationDuration, + child: !hasInternetConnection ? Text('No internet') : const SizedBox.shrink(), + ), + loginOptionsBody, + ], + ), + ), + ) + : loginOptionsBody, + ); + } +} diff --git a/lib/locator.dart b/lib/locator.dart index 1af5c77..7add9e3 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -1,8 +1,13 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get_it/get_it.dart'; +import 'package:internet_connection_checker/internet_connection_checker.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'; +import 'features/core/services/connection_service.dart'; +import 'features/core/services/overlay_service.dart'; + GetIt get locate => Locator.instance(); class Locator { @@ -14,8 +19,6 @@ class Locator { _registerAPIs(); _registerViewModels(); - _registerLazySingletons(); - _registerFactories(); await _registerServices(locator); @@ -27,10 +30,6 @@ class Locator { static void _registerViewModels() {} - static void _registerLazySingletons() {} - - static void _registerFactories() {} - static _registerServices(GetIt it) { it.registerLazySingleton( () => NavigationService( @@ -40,6 +39,18 @@ class Locator { it.registerFactory( () => LoggingService(), ); + it.registerLazySingleton( + () => ConnectionService( + connectivity: Connectivity(), + internetConnectionChecker: InternetConnectionChecker(), + ), + ); + it.registerLazySingleton( + () => OverlayService( + loggingService: LoggingService.locate, + ), + dispose: (param) => param.dispose(), + ); } static _registerRepos(GetIt locator) {} diff --git a/pubspec.lock b/pubspec.lock index d28cbf3..555f656 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,6 +71,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.3" convert: dependency: transitive description: @@ -99,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.8" dio: dependency: "direct main" description: @@ -113,6 +134,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" file: dependency: transitive description: @@ -169,7 +197,7 @@ packages: name: go_router url: "https://pub.dartlang.org" source: hosted - version: "5.2.4" + version: "6.0.0" http: dependency: transitive description: @@ -184,6 +212,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.2" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+1" intl: dependency: transitive description: @@ -240,6 +275,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" package_config: dependency: transitive description: @@ -275,6 +317,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" pointycastle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 758bda2..548be3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: cupertino_icons: ^1.0.5 # Routing - go_router: ^5.2.4 + go_router: ^6.0.0 # Service locator get_it: ^7.2.0 @@ -24,6 +24,8 @@ dependencies: # Util backend intl_utils: ^2.8.1 + connectivity_plus: ^3.0.2 + internet_connection_checker: ^1.0.0+1 # Logging talker: ^2.1.0+1