diff --git a/lib/features/core/data/constants/const_colors.dart b/lib/features/core/data/constants/const_colors.dart index 47465c2..cd6f427 100644 --- a/lib/features/core/data/constants/const_colors.dart +++ b/lib/features/core/data/constants/const_colors.dart @@ -7,7 +7,6 @@ abstract class ConstColours { static const white = Colors.white; static const black = Colors.black; - static const transparent = Colors.transparent; } abstract class ConstThemes { diff --git a/lib/features/core/services/connection_service.dart b/lib/features/core/services/connection_service.dart new file mode 100644 index 0000000..97ddd14 --- /dev/null +++ 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/connections_service.dart b/lib/features/core/services/connections_service.dart deleted file mode 100644 index c975563..0000000 --- a/lib/features/core/services/connections_service.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; - -import '/features/core/services/logging_service.dart'; -import '/locator.dart'; - -/// Used to observe the current connection type. -class ConnectionsService { - ConnectionsService({ - required Connectivity connectivity, - required InternetConnectionChecker internetConnectionChecker, - required LoggingService loggingService, - }) : _internetConnectionChecker = internetConnectionChecker, - _connectivity = connectivity, - _loggingService = loggingService { - _init(); - } - - final InternetConnectionChecker _internetConnectionChecker; - final Connectivity _connectivity; - final LoggingService _loggingService; - - late final ValueNotifier _internetConnectionStatusNotifier; - ValueListenable get internetConnectionStatusListenable => - _internetConnectionStatusNotifier; - late final ValueNotifier _connectivityResultNotifier; - ValueListenable get connectivityResultListenable => - _connectivityResultNotifier; - - Future _init() async { - // Initialize notifiers - _internetConnectionStatusNotifier = - ValueNotifier(await _internetConnectionChecker.connectionStatus); - _connectivityResultNotifier = ValueNotifier(await _connectivity.checkConnectivity()); - _loggingService - .info('Initial internet status: ${internetConnectionStatusListenable.value.nameWithIcon}'); - _loggingService - .info('Initial connectivity result: ${connectivityResultListenable.value.nameWithIcon}'); - - // Attach converters by listening to stream - _internetConnectionChecker.onStatusChange - .listen((final InternetConnectionStatus internetConnectionStatus) { - _loggingService.info( - 'Internet status changed to: ${internetConnectionStatus.nameWithIcon}. Notifying...'); - _internetConnectionStatusNotifier.value = internetConnectionStatus; - }); - _connectivity.onConnectivityChanged.listen((final connectivityResult) { - _loggingService - .info('Connectivity result changed to: ${connectivityResult.nameWithIcon}. Notifying...'); - _connectivityResultNotifier.value = connectivityResult; - }); - - Locator.instance().signalReady(this); - } - - Future dispose() async { - _internetConnectionStatusNotifier.dispose(); - _connectivityResultNotifier.dispose(); - } - - static ConnectionsService get locate => Locator.locate(); -} - -extension _connectionStatusEmojiExtension on InternetConnectionStatus { - String get nameWithIcon { - switch (this) { - case InternetConnectionStatus.connected: - return '$name (🌐✅)'; - case InternetConnectionStatus.disconnected: - return '$name (🔌)'; - } - } -} - -extension _connectivityEmojiExtension on ConnectivityResult { - String get nameWithIcon { - switch (this) { - case ConnectivityResult.bluetooth: - return '$name (ᛒ🟦)'; - case ConnectivityResult.wifi: - return '$name (◲)'; - case ConnectivityResult.ethernet: - return '$name (🌐)'; - case ConnectivityResult.mobile: - return '$name (📶)'; - case ConnectivityResult.none: - return '$name (🔌)'; - case ConnectivityResult.vpn: - return '$name (🔒)'; - } - } -} diff --git a/lib/features/core/services/overlay_service.dart b/lib/features/core/services/overlay_service.dart index aba06c9..1514c75 100644 --- a/lib/features/core/services/overlay_service.dart +++ b/lib/features/core/services/overlay_service.dart @@ -1,45 +1,36 @@ +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 { - OverlayService({ + const OverlayService({ required LoggingService loggingService, }) : _loggingService = loggingService; final LoggingService _loggingService; - final Map _overlayEntryMap = {}; + final Map _overlayEntryMap = const {}; - void insertOverlayEntry( - BuildContext context, { - required String tag, + Future playOverlayEntry({ + required BuildContext context, required OverlayEntry overlayEntry, - }) { - if (!_overlayEntryMap.containsKey(tag.hashCode) && !overlayEntry.mounted) { - _overlayEntryMap.addEntries([MapEntry(tag.hashCode, overlayEntry)]); - try { - Overlay.of(context, rootOverlay: true)?.insert(overlayEntry); - //todo(mehul): Fix and not ignore Overlay building while Widget building error. - } on FlutterError catch (_) {} - _loggingService.info('Overlay inserted with tag: $tag'); - } else - _loggingService.info('Overlay with tag: $tag, NOT inserted'); - } + }) async { + try { + _overlayEntryMap[overlayEntry.hashCode] = overlayEntry; + Overlay.of( + context, + rootOverlay: true, + )! + .insert(overlayEntry); - void removeOverlayEntry({ - required String tag, - }) { - if (_overlayEntryMap.containsKey(tag.hashCode)) { - final _overlayEntry = _overlayEntryMap[tag.hashCode]; - if (_overlayEntry?.mounted ?? false) { - _overlayEntryMap[tag.hashCode]?.remove(); - _overlayEntryMap.remove(tag.hashCode); - _loggingService.info('Overlay removed with tag: $tag'); - } else - _loggingService.info('Overlay with tag: $tag already mounted OR not found. Skipped'); - } else - _loggingService.info('Overlay with tag: $tag already exists. Skipped'); + if (overlayEntry.mounted) overlayEntry.remove(); + + _overlayEntryMap.remove(overlayEntry.hashCode); + } catch (error, stackTrace) { + _loggingService.handle(error, stackTrace); + } } void dispose() { diff --git a/lib/features/core/widgets/mcg_scaffold.dart b/lib/features/core/widgets/mcg_scaffold.dart index 2487280..2b15aa3 100644 --- a/lib/features/core/widgets/mcg_scaffold.dart +++ b/lib/features/core/widgets/mcg_scaffold.dart @@ -1,15 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; -import 'package:mc_gallery/features/core/data/constants/const_colors.dart'; -import 'package:mc_gallery/features/core/services/logging_service.dart'; -import 'package:mc_gallery/l10n/generated/l10n.dart'; +import 'package:flutter/widgets.dart'; -import '/features/core/services/connections_service.dart'; -import '/features/core/services/overlay_service.dart'; +import '../data/constants/const_durations.dart'; +import '../services/connection_service.dart'; -class McgScaffold extends StatelessWidget with LoggingService { - McgScaffold({ +class McgScaffold extends StatelessWidget { + const McgScaffold({ this.appBar, this.bodyBuilderWaiter, this.body, @@ -31,59 +28,36 @@ class McgScaffold extends StatelessWidget with LoggingService { /// Enabling listing to [ConnectionState], showing a small text at the top, when connectivity is lost. final bool forceInternetCheck; - final ConnectionsService _connectionsService = ConnectionsService.locate; - final OverlayService _overlayService = OverlayService.locate; @override Widget build(BuildContext context) { - if (forceInternetCheck) { - _connectionsService.internetConnectionStatusListenable.addListener( - () => _handleOverlayDisplay(context: context), - ); - if (_connectionsService.internetConnectionStatusListenable.value == - InternetConnectionStatus.disconnected) _handleOverlayDisplay(context: context); - } + final Widget loginOptionsBody = bodyBuilderWaiter != null + ? ValueListenableBuilder( + valueListenable: bodyBuilderWaiter!, + builder: (context, final isReady, child) => !isReady + ? Center(child: waitingWidget ?? const CircularProgressIndicator()) + : body ?? const SizedBox.shrink(), + ) + : body ?? const SizedBox.shrink(); return Scaffold( appBar: appBar, - body: bodyBuilderWaiter != null - ? ValueListenableBuilder( - valueListenable: bodyBuilderWaiter!, - builder: (context, final isReady, child) => !isReady - ? Center(child: waitingWidget ?? const CircularProgressIndicator()) - : body ?? const SizedBox.shrink(), - ) - : body ?? const SizedBox.shrink(), - ); - } - - void _handleOverlayDisplay({required BuildContext context}) { - switch (_connectionsService.internetConnectionStatusListenable.value) { - case InternetConnectionStatus.disconnected: - _overlayService.insertOverlayEntry( - context, - tag: runtimeType.toString(), - overlayEntry: OverlayEntry( - opaque: false, - builder: (_) => Align( - alignment: Alignment.topCenter, - child: Card( - elevation: 16, - surfaceTintColor: ConstColours.transparent, - color: ConstColours.transparent, - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - Strings.current.noInternetMessage, - ), + 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, + ], ), ), - ), - ), - ); - break; - case InternetConnectionStatus.connected: - _overlayService.removeOverlayEntry(tag: runtimeType.toString()); - } + ) + : loginOptionsBody, + ); } } diff --git a/lib/features/home/views/gallery/gallery_view.dart b/lib/features/home/views/gallery/gallery_view.dart index b517f53..cde03b3 100644 --- a/lib/features/home/views/gallery/gallery_view.dart +++ b/lib/features/home/views/gallery/gallery_view.dart @@ -16,7 +16,6 @@ class GalleryView extends StatelessWidget { viewModelBuilder: () => GalleryViewModel.locate, builder: (context, final model) => McgScaffold( bodyBuilderWaiter: model.isInitialised, - forceInternetCheck: true, appBar: AppBar( title: Text(model.strings.gallery), ), diff --git a/lib/features/home/views/image_carousel/image_carousel_view.dart b/lib/features/home/views/image_carousel/image_carousel_view.dart index 1292aeb..8b1d9b7 100644 --- a/lib/features/home/views/image_carousel/image_carousel_view.dart +++ b/lib/features/home/views/image_carousel/image_carousel_view.dart @@ -38,39 +38,37 @@ class ImageCarouselView extends StatelessWidget { body: Column( children: [ Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - elevation: 8, - surfaceTintColor: ConstColours.transparent, - child: CarouselSlider.builder( - itemCount: model.numberOfImages, - options: CarouselOptions( - enlargeFactor: 1, - enlargeCenterPage: true, - enlargeStrategy: CenterPageEnlargeStrategy.scale, - disableCenter: true, - viewportFraction: 1, - initialPage: model.currentImageIndex, - enableInfiniteScroll: false, - onPageChanged: (final index, _) => model.swipedTo(newIndex: index), - ), - itemBuilder: (context, _, __) => Stack( - fit: StackFit.expand, - children: [ - ValueListenableBuilder( - valueListenable: model.currentImageModelListenable, - builder: (context, _, __) => CachedNetworkImage( - imageUrl: model.currentImageUrl, - cacheKey: model.currentImageKey, - fit: BoxFit.contain, - progressIndicatorBuilder: (_, __, final progress) => - CircularProgressIndicator( - value: model.downloadProgressValue(progress: progress), - ), + child: Card( + elevation: 8, + child: CarouselSlider.builder( + itemCount: model.numberOfImages, + options: CarouselOptions( + enlargeFactor: 1, + enlargeCenterPage: true, + enlargeStrategy: CenterPageEnlargeStrategy.scale, + disableCenter: true, + aspectRatio: 1, + initialPage: model.currentImageIndex, + enableInfiniteScroll: false, + onPageChanged: (final index, _) => model.swipedTo(newIndex: index), + ), + itemBuilder: (context, _, __) => Stack( + fit: StackFit.expand, + children: [ + ValueListenableBuilder( + valueListenable: model.currentImageModelListenable, + builder: (context, _, __) => CachedNetworkImage( + imageUrl: model.currentImageUrl, + cacheKey: model.currentImageKey, + fit: BoxFit.fill, + progressIndicatorBuilder: (_, __, final progress) => + CircularProgressIndicator( + value: model.downloadProgressValue(progress: progress), ), ), - ValueListenableBuilder( + ), + Center( + child: ValueListenableBuilder( valueListenable: model.currentImageModelListenable, builder: (context, _, __) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -92,8 +90,8 @@ class ImageCarouselView extends StatelessWidget { ], ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/l10n/generated/intl/messages_en.dart b/lib/l10n/generated/intl/messages_en.dart index 46b3ea6..ed85749 100644 --- a/lib/l10n/generated/intl/messages_en.dart +++ b/lib/l10n/generated/intl/messages_en.dart @@ -27,8 +27,6 @@ class MessageLookup extends MessageLookupByLibrary { "imageCarousel": MessageLookupByLibrary.simpleMessage("Image carousel"), "imageDetails": MessageLookupByLibrary.simpleMessage( "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?"), - "noInternetMessage": MessageLookupByLibrary.simpleMessage( - "Are you sure that you\'re connected to the internet?"), "somethingWentWrong": MessageLookupByLibrary.simpleMessage("Something went wrong"), "startLoadingPrompt": diff --git a/lib/l10n/generated/l10n.dart b/lib/l10n/generated/l10n.dart index ec0ad09..7c4f39c 100644 --- a/lib/l10n/generated/l10n.dart +++ b/lib/l10n/generated/l10n.dart @@ -109,16 +109,6 @@ class Strings { args: [], ); } - - /// `Are you sure that you're connected to the internet?` - String get noInternetMessage { - return Intl.message( - 'Are you sure that you\'re connected to the internet?', - name: 'noInternetMessage', - desc: '', - args: [], - ); - } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9f04702..e08a39f 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -8,7 +8,5 @@ "startLoadingPrompt": "Press me to start loading", "imageCarousel": "Image carousel", - "imageDetails": "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?", - - "noInternetMessage": "Are you sure that you're connected to the internet?" + "imageDetails": "Lorem ipsum dolor sit amet. A odio aliquam est sunt explicabo cum galisum asperiores qui voluptas tempora qui aliquid similique. Ut quam laborum ex nostrum recusandae ab sunt ratione quo tempore corporis 33 voluptas nulla aut obcaecati perspiciatis.\n\nAd eveniet exercitationem ad odit quidem aut omnis corporis ea nulla illum qui quisquam temporibus? Est obcaecati similique et quisquam unde ea impedit mollitia ea accusamus natus hic doloribus quis! Et dolorem rerum id doloribus sint ea porro quia ut reprehenderit ratione?" } \ No newline at end of file diff --git a/lib/locator.dart b/lib/locator.dart index a0f6706..cb1b0f1 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -3,18 +3,18 @@ import 'dart:async'; 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 'package:mc_gallery/features/home/api/unsplash_images_api.dart'; +import 'package:mc_gallery/features/home/services/image_cache_manager_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/abstracts/router/app_router.dart'; import 'features/core/services/app_lifecycle_service.dart'; -import 'features/core/services/connections_service.dart'; -import 'features/core/services/logging_service.dart'; -import 'features/core/services/navigation_service.dart'; +import 'features/core/services/connection_service.dart'; import 'features/core/services/overlay_service.dart'; -import 'features/home/api/unsplash_images_api.dart'; -import 'features/home/services/image_cache_manager_service.dart'; -import 'features/home/services/images_service.dart'; -import 'features/home/views/gallery/gallery_view_model.dart'; -import 'features/home/views/image_carousel/image_carousel_view_model.dart'; GetIt get locate => Locator.instance(); @@ -58,48 +58,37 @@ class Locator { ); } - static FutureOr _registerServices(GetIt it) async { + static FutureOr _registerServices(GetIt it) { it.registerLazySingleton( () => NavigationService( mcgRouter: McgRouter.locate, ), ); - it.registerFactory( () => LoggingService(), ); - - it.registerSingleton( - ConnectionsService( + it.registerLazySingleton( + () => ConnectionService( connectivity: Connectivity(), internetConnectionChecker: InternetConnectionChecker(), - loggingService: LoggingService.locate, ), - signalsReady: true, - dispose: (final param) async => await param.dispose(), ); - await it.isReady(); - it.registerLazySingleton( () => OverlayService( loggingService: LoggingService.locate, ), dispose: (param) => param.dispose(), ); - - it.registerSingleton( + instance().registerSingleton( AppLifecycleService( loggingService: LoggingService.locate, ), - dispose: (final param) async => await param.dispose(), + dispose: (param) async => await param.dispose(), ); - - it.registerSingleton( + it.registerSingleton( ImagesService(imagesApi: UnsplashImagesApi.locate, loggingService: LoggingService.locate), signalsReady: true, ); - await it.isReady(); - it.registerSingleton( ImageCacheManagerService( appLifecycleService: AppLifecycleService.locate,