From 257a74ff6cc43cd8a6e919871d0f8ed3dc91f335 Mon Sep 17 00:00:00 2001 From: hectorAguero Date: Wed, 17 Apr 2024 02:26:06 -0400 Subject: [PATCH] Added UI Animations and ScreenSize reestructure --- lib/common_widgets/app_animation_wrapper.dart | 4 +- lib/common_widgets/app_async_widget.dart | 115 ++++++++++++ lib/common_widgets/app_cupertino_button.dart | 86 ++++++++- .../app_cupertino_sliver_navigation_bar.dart | 8 +- lib/common_widgets/app_loading_indicator.dart | 51 ++++++ lib/core/client_network_provider.dart | 26 +++ .../media_query_context_extension.dart | 40 ----- lib/features/home/home_page.dart | 8 +- .../widgets/adaptive_navigation_rail.dart | 16 +- .../adaptive_navigation_rail_footer.dart | 28 ++- .../details/instrument_details_page.dart | 6 +- .../widgets/instrument_details_summary.dart | 6 +- .../widgets/instrument_header_images.dart | 4 +- .../instruments/instruments_repo.dart | 36 ++-- .../instruments/instruments_tab_page.dart | 74 ++++---- lib/features/parades/parades_repo.dart | 42 +++-- lib/features/parades/parades_tab_page.dart | 107 ++++++------ lib/features/parades/widgets/parade_item.dart | 76 ++++---- lib/features/schools/schools_repo.dart | 32 ++-- lib/features/schools/schools_tab_page.dart | 21 +-- .../schools/schools_tab_providers.dart | 8 +- lib/features/schools/widgets/school_card.dart | 7 +- .../schools/widgets/school_filter_chips.dart | 4 +- .../schools/widgets/schools_empty_list.dart | 63 +++++++ .../schools/widgets/schools_tab_body.dart | 165 ++++++------------ .../schools/widgets/schools_tab_navbar.dart | 6 +- .../widgets/schools_tab_search_header.dart | 12 +- lib/initialization_page.dart | 12 +- lib/localization/app_en.arb | 1 - lib/main.dart | 13 ++ lib/router/go_router.dart | 2 +- lib/utils/screen_size.dart | 35 ++++ 32 files changed, 683 insertions(+), 431 deletions(-) create mode 100644 lib/common_widgets/app_async_widget.dart create mode 100644 lib/common_widgets/app_loading_indicator.dart delete mode 100644 lib/extensions/media_query_context_extension.dart create mode 100644 lib/features/schools/widgets/schools_empty_list.dart create mode 100644 lib/utils/screen_size.dart diff --git a/lib/common_widgets/app_animation_wrapper.dart b/lib/common_widgets/app_animation_wrapper.dart index 8a1aff9..72b7d7c 100644 --- a/lib/common_widgets/app_animation_wrapper.dart +++ b/lib/common_widgets/app_animation_wrapper.dart @@ -24,9 +24,9 @@ class _AppAnimationWrapperState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 350), )..forward(); - _animation = Tween(begin: 0.5, end: 1).animate( + _animation = Tween(begin: 0.6, end: 1).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOut, diff --git a/lib/common_widgets/app_async_widget.dart b/lib/common_widgets/app_async_widget.dart new file mode 100644 index 0000000..b8ae59b --- /dev/null +++ b/lib/common_widgets/app_async_widget.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +import '../extensions/app_localization_extension.dart'; +import 'app_cupertino_button.dart'; + +class AppAsyncWidget extends StatelessWidget { + const AppAsyncWidget({ + required this.asyncValue, + required this.child, + super.key, + this.onErrorRetry, + }); + + final AsyncValue asyncValue; + final VoidCallback? onErrorRetry; + final Widget child; + + @override + Widget build(BuildContext context) { + return switch (asyncValue) { + AsyncData() => child, + AsyncError(:final error) => Center( + key: const ValueKey('error'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + error.toString(), + style: Theme.of(context).textTheme.titleLarge, + ), + if (onErrorRetry != null) + CupertinoButton( + onPressed: onErrorRetry, + child: Text(context.loc.retry), + ), + ], + ), + ), + AsyncLoading() => const Center( + key: ValueKey('loading'), + child: Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ), + }; + } +} + +class AppAsyncSliver extends StatelessWidget { + const AppAsyncSliver({ + required this.asyncValue, + required this.child, + this.onErrorRetry, + super.key, + }); + + final AsyncValue asyncValue; + final VoidCallback? onErrorRetry; + final Widget Function(T) child; + + @override + Widget build(BuildContext context) { + return SliverAnimatedSwitcher( + duration: kThemeAnimationDuration, + child: switch (asyncValue) { + AsyncData(:final value) => child(value), + AsyncError(:final error) => SliverFillRemaining( + key: const ValueKey('error'), + child: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + error.toString(), + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + if (onErrorRetry != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: AppCupertinoButton.tinted( + color: Theme.of(context).colorScheme.primary, + onPressed: onErrorRetry, + icon: const Icon(Icons.refresh), + child: Text(context.loc.retry), + ), + ), + ], + ), + ), + ), + ), + AsyncLoading() => const SliverFillRemaining( + key: ValueKey('loading'), + child: Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ), + }, + ); + } +} diff --git a/lib/common_widgets/app_cupertino_button.dart b/lib/common_widgets/app_cupertino_button.dart index 72eafbe..e840c4a 100644 --- a/lib/common_widgets/app_cupertino_button.dart +++ b/lib/common_widgets/app_cupertino_button.dart @@ -1,35 +1,103 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../extensions/theme_of_context_extension.dart'; ///extended CupertinoButton to pass null values in the minimumSize and padding +enum CupertinoButtonType { + plain, + gray, + tinted, + filled, +} + class AppCupertinoButton extends StatelessWidget { const AppCupertinoButton({ required this.child, required this.onPressed, + super.key, this.color, this.disabledColor, - this.padding, + this.icon, + this.padding = EdgeInsets.zero, this.minSize, + this.borderRadius = const BorderRadius.all(Radius.circular(32)), + this.type, + }); + + const AppCupertinoButton.tinted({ + required this.child, + required this.onPressed, + required this.color, super.key, + this.disabledColor, + this.icon, + this.borderRadius = const BorderRadius.all(Radius.circular(12)), + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + this.type = CupertinoButtonType.tinted, + this.minSize, }); final Widget child; + final Widget? icon; final VoidCallback? onPressed; final EdgeInsetsGeometry? padding; final double? minSize; final Color? color; final Color? disabledColor; + final BorderRadius borderRadius; + final CupertinoButtonType? type; @override Widget build(BuildContext context) { - return CupertinoButton( - disabledColor: disabledColor ?? CupertinoColors.quaternarySystemFill, - onPressed: onPressed, - padding: padding ?? EdgeInsets.zero, - minSize: minSize ?? 0, - borderRadius: BorderRadius.circular(32), - color: color, - child: child, + final typeSize = type == null ? 0.0 : kMinInteractiveDimensionCupertino; + return Theme( + data: Theme.of(context).copyWith( + cupertinoOverrideTheme: CupertinoThemeData( + primaryColor: color ?? context.colorScheme.primary, + primaryContrastingColor: color ?? context.colorScheme.primary, + scaffoldBackgroundColor: color ?? context.colorScheme.primary, + textTheme: const CupertinoTextThemeData( + primaryColor: Colors.white, + textStyle: TextStyle(), + ), + ), + ), + child: Builder( + builder: (context) { + return CupertinoButton( + disabledColor: + disabledColor ?? CupertinoColors.quaternarySystemFill, + onPressed: onPressed, + padding: padding, + minSize: minSize ?? typeSize, + borderRadius: borderRadius, + color: _calculateColor(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 4), + child: icon, + ), + child, + ], + ), + ); + }, + ), ); } + + Color? _calculateColor(BuildContext context) { + return switch (type) { + CupertinoButtonType.plain || null => null, + CupertinoButtonType.gray => CupertinoColors.systemFill, + CupertinoButtonType.tinted => color!.withOpacity(0.2), + CupertinoButtonType.filled => context.colorScheme.primary.darken() + }; + } } diff --git a/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart b/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart index 4a10bff..cf146f7 100644 --- a/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart +++ b/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart @@ -1,14 +1,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../extensions/is_ios_or_macos_platform_extension.dart'; +import '../extensions/is_ios_or_macos_platform_extension.dart'; import '../extensions/js_bottom_padding_extension.dart' if (dart.library.js_interop) '../extensions/js_bottom_padding_extension_web.dart'; - -import '../extensions/media_query_context_extension.dart'; import '../extensions/theme_of_context_extension.dart'; import '../features/home/widgets/settings_modal_sheet.dart'; +import '../utils/screen_size.dart'; class AppCupertinoSliverNavigationBar extends StatelessWidget { const AppCupertinoSliverNavigationBar({ @@ -26,6 +25,7 @@ class AppCupertinoSliverNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { + final screenSize = context.screenSize; return CupertinoSliverNavigationBar( backgroundColor: Colors.transparent, largeTitle: Text( @@ -47,7 +47,7 @@ class AppCupertinoSliverNavigationBar extends StatelessWidget { start: 16, ) : const EdgeInsetsDirectional.symmetric(horizontal: 16), - trailing: context.querySize.isSmallScreen + trailing: screenSize.isSmall ? CupertinoButton( padding: EdgeInsets.zero, borderRadius: BorderRadius.zero, diff --git a/lib/common_widgets/app_loading_indicator.dart b/lib/common_widgets/app_loading_indicator.dart new file mode 100644 index 0000000..188c040 --- /dev/null +++ b/lib/common_widgets/app_loading_indicator.dart @@ -0,0 +1,51 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:flutter/material.dart'; + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({ + required this.showLoading, + this.sliver = false, + super.key, + }); + + final bool showLoading; + final bool sliver; + @override + Widget build(BuildContext context) { + if (sliver) { + return SliverToBoxAdapter( + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: !showLoading + ? const SizedBox.shrink() + : const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + height: 32, + width: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ], + ), + ), + ); + } + return AnimatedSize( + duration: const Duration(milliseconds: 300), + child: !showLoading + ? const SizedBox.shrink() + : const Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + height: 32, + width: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ); + } +} diff --git a/lib/core/client_network_provider.dart b/lib/core/client_network_provider.dart index a6ceb6f..38c8af4 100644 --- a/lib/core/client_network_provider.dart +++ b/lib/core/client_network_provider.dart @@ -87,3 +87,29 @@ enum Endpoint { schools => '/schools', }; } + +class AppNetworkError extends Error { + AppNetworkError(this.message); + + AppNetworkError.fromNetworkClientException(Object e) + : message = messageFromDio(e); + + final String message; + + @override + String toString() => message; + + static String messageFromDio(Object e) { + if (e is! DioException) return 'Unknown error 🤷'; + return switch (e.type) { + DioExceptionType.badCertificate => 'Bad certificate 📜', + DioExceptionType.connectionTimeout => 'Connection timeout ⏰', + DioExceptionType.sendTimeout => 'Send timeout ⏰', + DioExceptionType.receiveTimeout => 'Receive timeout ⏰', + DioExceptionType.badResponse => 'Bad response 🤷', + DioExceptionType.cancel => 'Request cancelled 🚫', + DioExceptionType.connectionError => 'Connection error 🚫', + DioExceptionType.unknown => 'No internet connection 🌎', + }; + } +} diff --git a/lib/extensions/media_query_context_extension.dart b/lib/extensions/media_query_context_extension.dart deleted file mode 100644 index 39dcffe..0000000 --- a/lib/extensions/media_query_context_extension.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; -import '../features/home/widgets/adaptive_navigation_rail.dart'; - -const double smallHeight = 500; -const double smallScreen = 600; -const double mediumScreen = 900; -const double largeScreen = 1200; -const double extraLargeScreen = 1536; - -extension MediaQueryExtension on BuildContext { - Size get querySize => MediaQuery.sizeOf(this); -} - -extension ScreenSizeExtension on Size { - bool get isSmallScreen => width < smallScreen; - - bool get isMediumScreen => width >= smallScreen && width < mediumScreen; - - bool get isLargeScreen => width >= mediumScreen && width < largeScreen; - - bool get isExtraLargeScreen => width >= largeScreen; - - bool get isNotSmallNorMedium => !isSmallScreen && !isMediumScreen; - - int crossAxisCount([int small = 1, int large = 2, int extraLarge = 3]) { - if (isSmallScreen || isMediumScreen) { - return small; - } else if (isLargeScreen) { - return large; - } else { - return extraLarge; - } - } - - double get currentRailWidth { - if (isSmallScreen) return 0; - if (isMediumScreen) return AdaptiveNavigationRail.smallRailWidth; - return AdaptiveNavigationRail.largeRailWidth; - } -} diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart index 84d94e4..fde8ae1 100644 --- a/lib/features/home/home_page.dart +++ b/lib/features/home/home_page.dart @@ -4,9 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../common_widgets/app_web_padding.dart'; -import '../../extensions/media_query_context_extension.dart'; import '../../extensions/theme_of_context_extension.dart'; import '../../utils/immutable_list.dart'; +import '../../utils/screen_size.dart'; import 'home_page_controller.dart'; import 'widgets/adaptive_navigation_bar.dart'; import 'widgets/adaptive_navigation_rail.dart'; @@ -18,9 +18,9 @@ class HomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final size = MediaQuery.sizeOf(context); + final screenSize = context.screenSize; return Scaffold( - body: size.isSmallScreen + body: screenSize.isSmall ? navigationShell : AppWebPadding.only( left: true, @@ -39,7 +39,7 @@ class HomePage extends ConsumerWidget { ], ), ), - bottomNavigationBar: !size.isSmallScreen + bottomNavigationBar: screenSize.isMedium || screenSize.isLarge ? null : Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/features/home/widgets/adaptive_navigation_rail.dart b/lib/features/home/widgets/adaptive_navigation_rail.dart index 777b1c7..7117459 100644 --- a/lib/features/home/widgets/adaptive_navigation_rail.dart +++ b/lib/features/home/widgets/adaptive_navigation_rail.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import '../../../extensions/media_query_context_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; import '../../../utils/immutable_list.dart'; +import '../../../utils/screen_size.dart'; import '../home_page_controller.dart'; import 'adaptive_navigation_rail_footer.dart'; @@ -23,30 +23,30 @@ class AdaptiveNavigationRail extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - final footerHeight = size.height <= smallHeight + final height = MediaQuery.sizeOf(context).height; + final screenSize = context.screenSize; + final footerHeight = height <= ScreenSize.smallHeight ? AdaptiveNavigationRailFooter.heightCollapsed : AdaptiveNavigationRailFooter.heightFull; - final isLargeOrExtraLarge = size.isLargeScreen || size.isExtraLargeScreen; return ColoredBox( color: context.colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ SizedBox( - height: size.height - footerHeight, + height: height - footerHeight, child: NavigationRail( leading: Padding( padding: const EdgeInsets.all(8), child: Icon( Icons.library_music, color: context.colorScheme.primary, - size: (context.querySize.height * 0.1).clamp(24, 80), + size: (height * 0.1).clamp(24, 80), ), ), backgroundColor: context.colorScheme.surface, onDestinationSelected: onDestinationSelected, - extended: size.isLargeScreen || size.isExtraLargeScreen, + extended: screenSize.isLarge, selectedIndex: selectedIndex, indicatorShape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -68,7 +68,7 @@ class AdaptiveNavigationRail extends StatelessWidget { color: context.colorScheme.surface, child: SizedBox( height: footerHeight, - width: isLargeOrExtraLarge ? largeRailWidth : smallRailWidth, + width: screenSize.isLarge ? largeRailWidth : smallRailWidth, child: const AdaptiveNavigationRailFooter(), ), ), diff --git a/lib/features/home/widgets/adaptive_navigation_rail_footer.dart b/lib/features/home/widgets/adaptive_navigation_rail_footer.dart index c4b362e..5268e65 100644 --- a/lib/features/home/widgets/adaptive_navigation_rail_footer.dart +++ b/lib/features/home/widgets/adaptive_navigation_rail_footer.dart @@ -4,11 +4,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pull_down_button/pull_down_button.dart'; import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; import '../../../localization/language.dart'; import '../../../localization/language_app_provider.dart'; import '../../../theme/theme_provider.dart'; +import '../../../utils/screen_size.dart'; import 'settings_modal_sheet.dart'; class AdaptiveNavigationRailFooter extends ConsumerWidget { @@ -20,10 +20,10 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final locale = WidgetsBinding.instance.platformDispatcher.locale; - final size = MediaQuery.sizeOf(context); + final screenSize = context.screenSize; final themeMode = ref.watch(appThemeModeProvider); final trueBlack = ref.watch(appThemeTrueBlackProvider); - if (size.height < smallHeight) { + if (MediaQuery.sizeOf(context).height < ScreenSize.smallHeight) { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -33,15 +33,13 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 24), ), child: CupertinoListTile.notched( - title: size.isLargeScreen || size.isExtraLargeScreen + title: screenSize.isLarge ? Text( context.loc.settingsTitle, style: context.textTheme.titleMedium, ) : const Icon(CupertinoIcons.settings), - leading: size.isLargeScreen || size.isExtraLargeScreen - ? Icon(themeMode.icon) - : null, + leading: screenSize.isLarge ? Icon(themeMode.icon) : null, ), ), ], @@ -72,13 +70,13 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { ], buttonBuilder: (context, showMenu) => CupertinoListTile.notched( onTap: showMenu, - leading: size.isLargeScreen || size.isExtraLargeScreen + leading: screenSize.isLarge ? Icon( CupertinoIcons.flag, color: context.colorScheme.onSurface, ) : null, - title: size.isMediumScreen + title: !screenSize.isLarge ? Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( @@ -103,7 +101,7 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { .toggleTrueBlack(), child: CupertinoListTile.notched( trailing: IgnorePointer( - child: size.isMediumScreen + child: screenSize.isMedium ? null : Switch.adaptive( value: trueBlack, @@ -115,7 +113,7 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { .toggleTrueBlack(), ), ), - leading: size.isLargeScreen || size.isExtraLargeScreen + leading: screenSize.isLarge ? Icon( CupertinoIcons.moon_stars, color: themeMode.isLight @@ -123,7 +121,7 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { : context.colorScheme.onSurface, ) : null, - title: size.isMediumScreen + title: screenSize.isMedium ? Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( @@ -146,7 +144,7 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { onTap: () async => ref.read(appThemeModeProvider.notifier).toggleTheme(), child: CupertinoListTile.notched( - title: size.isLargeScreen || size.isExtraLargeScreen + title: screenSize.isLarge ? Text( themeMode.label(context), style: context.textTheme.titleMedium, @@ -155,9 +153,7 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon(themeMode.icon), ), - leading: size.isLargeScreen || size.isExtraLargeScreen - ? Icon(themeMode.icon) - : null, + leading: screenSize.isLarge ? Icon(themeMode.icon) : null, ), ), ], diff --git a/lib/features/instruments/details/instrument_details_page.dart b/lib/features/instruments/details/instrument_details_page.dart index 32becd6..407d32c 100644 --- a/lib/features/instruments/details/instrument_details_page.dart +++ b/lib/features/instruments/details/instrument_details_page.dart @@ -6,7 +6,7 @@ import '../../../common_widgets/app_back_button.dart'; import '../../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../../common_widgets/app_web_padding.dart'; import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; +import '../../../utils/screen_size.dart'; import 'instrument_details_providers.dart'; import 'widgets/instrument_details_summary.dart'; import 'widgets/instrument_header_images.dart'; @@ -32,8 +32,8 @@ class _InstrumentDetailsPageState extends ConsumerState { @override Widget build(BuildContext context) { final value = ref.watch(instrumentDetailsProvider(widget.id)); + final screenConstraint = ScreenSize.lg.value; const imageHeight = 80.0; - const screenConstraint = largeScreen; return DefaultTabController( length: InstrumentDetailsTab.values.length, child: Scaffold( @@ -80,7 +80,7 @@ class _InstrumentDetailsPageState extends ConsumerState { WebPaddingSliver.only( right: true, sliver: SliverCrossAxisConstrained( - maxCrossAxisExtent: smallScreen, + maxCrossAxisExtent: ScreenSize.md.value, child: SliverPadding( padding: const EdgeInsets.symmetric( horizontal: 24, diff --git a/lib/features/instruments/details/widgets/instrument_details_summary.dart b/lib/features/instruments/details/widgets/instrument_details_summary.dart index 5371e44..1b8676b 100644 --- a/lib/features/instruments/details/widgets/instrument_details_summary.dart +++ b/lib/features/instruments/details/widgets/instrument_details_summary.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../../../extensions/media_query_context_extension.dart'; +import '../../../../utils/screen_size.dart'; class InstrumentDetailsSummary extends StatelessWidget { const InstrumentDetailsSummary({ @@ -13,8 +13,8 @@ class InstrumentDetailsSummary extends StatelessWidget { Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final padding = - (constraints.maxWidth - mediumScreen).clamp(16.0, mediumScreen); + final padding = (constraints.maxWidth - ScreenSize.md.value) + .clamp(16.0, ScreenSize.md.value); return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 16, horizontal: padding), physics: const NeverScrollableScrollPhysics(), diff --git a/lib/features/instruments/details/widgets/instrument_header_images.dart b/lib/features/instruments/details/widgets/instrument_header_images.dart index d61f9ca..a3fcf44 100644 --- a/lib/features/instruments/details/widgets/instrument_header_images.dart +++ b/lib/features/instruments/details/widgets/instrument_header_images.dart @@ -3,9 +3,9 @@ import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import '../../../../common_widgets/app_fade_in_image.dart'; -import '../../../../extensions/media_query_context_extension.dart'; import '../../../../extensions/theme_of_context_extension.dart'; import '../../../../utils/immutable_list.dart'; +import '../../../../utils/screen_size.dart'; import '../../instrument.dart'; class InstrumentHeaderImages extends StatelessWidget { @@ -21,7 +21,7 @@ class InstrumentHeaderImages extends StatelessWidget { @override Widget build(BuildContext context) { - final imageQuantity = context.querySize.isSmallScreen ? 2 : 3; + final imageQuantity = context.screenSize.isSmall ? 2 : 3; final largeImageHeight = imageHeight * imageQuantity + (imageQuantity - 1) * 16; return Padding( diff --git a/lib/features/instruments/instruments_repo.dart b/lib/features/instruments/instruments_repo.dart index 3906f92..8252065 100644 --- a/lib/features/instruments/instruments_repo.dart +++ b/lib/features/instruments/instruments_repo.dart @@ -25,22 +25,32 @@ class InstrumentRepoImpls implements InstrumentsRepo { @override Future> getInstruments() async { - final response = await ref - .watch(clientNetworkProvider) - .value! - .get>(Endpoint.instruments.path); - final data = response.data!.cast>(); - return ImmutableList([ - for (final item in data) Instrument.fromMap(item), - ]); + try { + final response = await ref + .watch(clientNetworkProvider) + .value! + .get>(Endpoint.instruments.path); + final data = response.data!.cast>(); + return ImmutableList([ + for (final item in data) Instrument.fromMap(item), + ]); + } catch (e) { + throw AppNetworkError.fromNetworkClientException(e); + } } @override Future getDetails(InstrumentId id) async { - final response = - await ref.watch(clientNetworkProvider).value!.get>( - '${Endpoint.instruments.pathId}/$id', - ); - return Instrument.fromMap(response.data!); + try { + final response = await ref + .watch(clientNetworkProvider) + .value! + .get>( + '${Endpoint.instruments.pathId}/$id', + ); + return Instrument.fromMap(response.data!); + } catch (e) { + throw AppNetworkError.fromNetworkClientException(e); + } } } diff --git a/lib/features/instruments/instruments_tab_page.dart b/lib/features/instruments/instruments_tab_page.dart index d8ae007..c2584bd 100644 --- a/lib/features/instruments/instruments_tab_page.dart +++ b/lib/features/instruments/instruments_tab_page.dart @@ -4,10 +4,11 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:go_router/go_router.dart'; import 'package:sliver_tools/sliver_tools.dart'; +import '../../common_widgets/app_async_widget.dart'; import '../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../common_widgets/app_web_padding.dart'; import '../../extensions/app_localization_extension.dart'; -import '../../extensions/media_query_context_extension.dart'; +import '../../utils/screen_size.dart'; import '../home/home_page_controller.dart'; import 'instruments_tab_providers.dart'; import 'widgets/instrument_list_tile.dart'; @@ -20,13 +21,11 @@ class InstrumentsTabPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - const maxCrossAxisExtent = largeScreen; - final instruments = ref.watch(instrumentsTabProvider); return Scaffold( body: CustomScrollView( slivers: [ SliverCrossAxisConstrained( - maxCrossAxisExtent: maxCrossAxisExtent, + maxCrossAxisExtent: ScreenSize.lg.value, child: SliverMainAxisGroup( slivers: [ AppCupertinoSliverNavigationBar( @@ -35,48 +34,35 @@ class InstrumentsTabPage extends ConsumerWidget { const SliverPadding(padding: EdgeInsets.only(top: 8)), SliverAnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: switch (instruments) { - AsyncLoading() => const SliverFillRemaining( - key: ValueKey('loading'), - child: Center( - child: CircularProgressIndicator.adaptive(), + child: AppAsyncSliver( + asyncValue: ref.watch(instrumentsTabProvider), + onErrorRetry: () => ref.invalidate(instrumentsTabProvider), + child: (value) => WebPaddingSliver.only( + right: true, + sliver: SliverSafeArea( + top: false, + sliver: SliverAlignedGrid.extent( + maxCrossAxisExtent: InstrumentListTile.cardMaxWidth, + itemCount: value.length, + itemBuilder: (context, index) { + final instrument = value[index]; + return InstrumentListTile( + title: instrument.translatedName, + originalTitle: instrument.name, + subtitle: instrument.translatedDescription, + index: index, + onTap: () { + context.go( + '$path/details/${instrument.id}', + ); + }, + imageUrl: instrument.imageUrl, + ); + }, ), ), - AsyncError(:final error) => SliverFillRemaining( - key: const ValueKey('error'), - child: Center( - child: Text( - error.toString(), - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - AsyncData(:final value) => WebPaddingSliver.only( - right: true, - sliver: SliverSafeArea( - top: false, - sliver: SliverAlignedGrid.extent( - maxCrossAxisExtent: InstrumentListTile.cardMaxWidth, - itemCount: value.length, - itemBuilder: (context, index) { - final instrument = value[index]; - return InstrumentListTile( - title: instrument.translatedName, - originalTitle: instrument.name, - subtitle: instrument.translatedDescription, - index: index, - onTap: () { - context.go( - '$path/details/${instrument.id}', - ); - }, - imageUrl: instrument.imageUrl, - ); - }, - ), - ), - ), - }, + ), + ), ), ], ), diff --git a/lib/features/parades/parades_repo.dart b/lib/features/parades/parades_repo.dart index df0d57c..0517bd6 100644 --- a/lib/features/parades/parades_repo.dart +++ b/lib/features/parades/parades_repo.dart @@ -28,26 +28,34 @@ class ParadesRepoImpl implements ParadesRepo { Future> getParades({ ParadeQueryParams? queryParams, }) async { - final response = - await ref.watch(clientNetworkProvider).value!.get>( - Endpoint.parades.path, - queryParameters: { - if (queryParams?.page != null) 'page': queryParams!.page, - if (queryParams?.pageSize != null) 'pageSize': queryParams!.pageSize, - }, - ); - final data = response.data!.cast>(); - return ImmutableList([ - for (final item in data) Parade.fromMap(item), - ]); + try { + final response = + await ref.watch(clientNetworkProvider).value!.get>( + Endpoint.parades.path, + queryParameters: { + if (queryParams?.page != null) 'page': queryParams!.page, + if (queryParams?.pageSize != null) 'pageSize': queryParams!.pageSize, + }, + ); + final data = response.data!.cast>(); + return ImmutableList([ + for (final item in data) Parade.fromMap(item), + ]); + } catch (e) { + throw AppNetworkError.fromNetworkClientException(e); + } } @override Future getParade(int id, {ParadeQueryParams? queryParams}) async { - final response = await ref - .watch(clientNetworkProvider) - .value! - .get>('${Endpoint.parades.pathId}/$id'); - return Parade.fromMap(response.data!); + try { + final response = await ref + .watch(clientNetworkProvider) + .value! + .get>('${Endpoint.parades.pathId}/$id'); + return Parade.fromMap(response.data!); + } catch (e) { + throw AppNetworkError.fromNetworkClientException(e); + } } } diff --git a/lib/features/parades/parades_tab_page.dart b/lib/features/parades/parades_tab_page.dart index 5a49863..e2c4521 100644 --- a/lib/features/parades/parades_tab_page.dart +++ b/lib/features/parades/parades_tab_page.dart @@ -3,14 +3,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; +import '../../common_widgets/app_animation_wrapper.dart'; +import '../../common_widgets/app_async_widget.dart'; import '../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; +import '../../common_widgets/app_loading_indicator.dart'; import '../../extensions/app_localization_extension.dart'; -import '../../extensions/media_query_context_extension.dart'; -import '../../extensions/theme_of_context_extension.dart'; import '../../utils/debouncer.dart'; +import '../../utils/screen_size.dart'; import '../home/home_page_controller.dart'; +import '../schools/school.dart'; import 'parades_tab_providers.dart'; import 'widgets/parade_item.dart'; +import 'widgets/parade_item_year_line.dart'; class ParadesTabPage extends ConsumerStatefulWidget { const ParadesTabPage({super.key}); @@ -55,75 +59,66 @@ class _ParadesTabPageState extends ConsumerState { @override Widget build(BuildContext context) { - final paradesFuture = ref.watch(paradesProvider); - final reachedLimit = ref.watch(paradesTabReachedLimitProvider); return Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.miniCenterTop, body: CustomScrollView( controller: controller, slivers: [ SliverCrossAxisConstrained( - maxCrossAxisExtent: mediumScreen, + maxCrossAxisExtent: ScreenSize.md.value, child: AppCupertinoSliverNavigationBar( largeTitle: context.loc.paradesTitle, ), ), - SliverAnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: switch (paradesFuture) { - AsyncError(:final error) => SliverFillRemaining( - key: const ValueKey('parades_error'), - child: Center( - child: Text( - error.toString(), - style: context.textTheme.titleLarge, - ), - ), - ), - AsyncLoading() => const SliverFillRemaining( - key: ValueKey('parades_loading'), - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), - AsyncData(:final value) => SliverCrossAxisConstrained( - maxCrossAxisExtent: mediumScreen, - child: SuperSliverList.builder( - itemCount: value.length, - listController: _listController, - itemBuilder: (context, index) { - return ProviderScope( - overrides: [ - currentParadeProvider.overrideWithValue(value[index]), - ], - child: Padding( - padding: index != 0 - ? EdgeInsets.zero - : const EdgeInsets.only(top: 24), - child: const ParadeItem(), - ), - ); - }, - ), - ), - }, - ), - SliverToBoxAdapter( - child: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: reachedLimit - ? const SizedBox.shrink() - : const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - height: 16, - child: CircularProgressIndicator.adaptive(), + AppAsyncSliver( + asyncValue: ref.watch(paradesProvider), + onErrorRetry: () => ref.invalidate(paradesProvider), + child: (value) => SliverCrossAxisConstrained( + maxCrossAxisExtent: ScreenSize.md.value, + child: SuperSliverList.builder( + itemCount: value.length, + listController: _listController, + itemBuilder: (context, index) { + final parade = value[index]; + return ProviderScope( + overrides: [ + currentParadeProvider.overrideWithValue(value[index]), + ], + child: Padding( + padding: index != 0 + ? EdgeInsets.zero + : const EdgeInsets.only(top: 24), + child: SizedBox( + height: ParadeItem.height, + child: Row( + children: [ + ParadeItemYearLine( + year: parade.champion && + parade.divisionNumber == + SchoolDivision.especial + ? parade.paradeYear.toString() + : null, + ), + const Expanded( + child: AppAnimationWrapper( + child: Expanded( + child: ParadeItem(), + ), + ), + ), + ], ), ), ), + ); + }, + ), ), ), + AppLoadingIndicator( + showLoading: !ref.watch(paradesTabReachedLimitProvider), + sliver: true, + ), ], ), ); diff --git a/lib/features/parades/widgets/parade_item.dart b/lib/features/parades/widgets/parade_item.dart index 4869f66..b72991b 100644 --- a/lib/features/parades/widgets/parade_item.dart +++ b/lib/features/parades/widgets/parade_item.dart @@ -7,69 +7,53 @@ import '../../../common_widgets/app_animated_linear_gradient.dart'; import '../../../common_widgets/app_fade_in_image.dart'; import '../../../extensions/app_localization_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; -import '../../schools/school.dart'; import '../../schools/school_extensions.dart'; import '../parade.dart'; import '../parade_extension.dart'; import '../parades_tab_providers.dart'; import 'parade_item_bottom_row.dart'; import 'parade_item_sidebar.dart'; -import 'parade_item_year_line.dart'; class ParadeItem extends ConsumerWidget { const ParadeItem({super.key}); + static const double height = 248; + @override Widget build(BuildContext context, WidgetRef ref) { final parade = ref.watch(currentParadeProvider); final medalColor = parade.medalColor(context); - return SafeArea( - top: false, - bottom: false, - child: SizedBox( - height: 248, - child: Row( - children: [ - ParadeItemYearLine( - year: parade.champion && - parade.divisionNumber == SchoolDivision.especial - ? parade.paradeYear.toString() - : null, + return Card( + clipBehavior: Clip.antiAlias, + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: InkWell( + onLongPress: () { + ref.read(paradeShowOriginalProvider.notifier).toggle(); + }, + child: Container( + height: double.infinity, + decoration: BoxDecoration( + gradient: RadialGradient( + colors: [ + medalColor.withOpacity(0.25), + medalColor.withOpacity(0.4), + ], ), - Expanded( - child: Card( - clipBehavior: Clip.antiAlias, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: InkWell( - onLongPress: () { - ref.read(paradeShowOriginalProvider.notifier).toggle(); - }, - child: Container( - height: double.infinity, - decoration: BoxDecoration( - gradient: RadialGradient( - colors: [ - medalColor.withOpacity(0.25), - medalColor.withOpacity(0.4), - ], - ), - ), - child: Row( - children: [ - ParadeItemSideBar( - placing: parade.placing, - parade: parade, - ), - Expanded( - child: ParadeItemContent(parade: parade), - ), - ], - ), - ), + ), + child: SizedBox( + height: height, + child: Row( + children: [ + ParadeItemSideBar( + placing: parade.placing, + parade: parade, ), - ), + Expanded( + child: ParadeItemContent(parade: parade), + ), + ], ), - ], + ), ), ), ); diff --git a/lib/features/schools/schools_repo.dart b/lib/features/schools/schools_repo.dart index 2645d46..8e8eac0 100644 --- a/lib/features/schools/schools_repo.dart +++ b/lib/features/schools/schools_repo.dart @@ -32,19 +32,23 @@ class SchoolsRepoImpls implements SchoolsRepo { required String sort, required String search, }) async { - final networkClient = ref.watch(clientNetworkProvider).requireValue; - final response = await networkClient.get>( - search.isEmpty ? Endpoint.schools.path : Endpoint.schools.pathSearch, - queryParameters: { - 'page': page, - 'pageSize': pageSize, - if (search.isNotEmpty) 'search': search, - // if (sort.isNotEmpty) 'sort': sort, - }, - ); - final data = response.data!.cast>(); - return ImmutableList([ - for (final item in data) School.fromMap(item), - ]); + try { + final networkClient = ref.watch(clientNetworkProvider).requireValue; + final response = await networkClient.get>( + search.isEmpty ? Endpoint.schools.path : Endpoint.schools.pathSearch, + queryParameters: { + 'page': page, + 'pageSize': pageSize, + if (search.isNotEmpty) 'search': search, + // if (sort.isNotEmpty) 'sort': sort, + }, + ); + final data = response.data!.cast>(); + return ImmutableList([ + for (final item in data) School.fromMap(item), + ]); + } catch (e) { + throw AppNetworkError.fromNetworkClientException(e); + } } } diff --git a/lib/features/schools/schools_tab_page.dart b/lib/features/schools/schools_tab_page.dart index 1012388..1b3c5b4 100644 --- a/lib/features/schools/schools_tab_page.dart +++ b/lib/features/schools/schools_tab_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../common_widgets/app_loading_indicator.dart'; import '../../utils/debouncer.dart'; import '../home/home_page_controller.dart'; import 'schools_tab_providers.dart'; @@ -74,22 +75,12 @@ class SchoolsTabLoadMoreIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final reachedLimit = ref.watch(schoolReachedMaxProvider); - final isEmpty = ref.watch(filteredSchoolsProvider).isEmpty; + final isNotEmpty = ref.watch(filteredSchoolsProvider).isNotEmpty; final isFiltered = ref.watch(filteredSchoolsProvider).length != - ref.watch(schoolsProvider).value?.length; - return SliverToBoxAdapter( - child: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: reachedLimit || isEmpty || isFiltered - ? const SizedBox.shrink() - : const Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - height: 16, - child: CircularProgressIndicator.adaptive(), - ), - ), - ), + ref.watch(schoolsProvider).valueOrNull?.length; + return AppLoadingIndicator( + showLoading: !reachedLimit && isNotEmpty && !isFiltered, + sliver: true, ); } } diff --git a/lib/features/schools/schools_tab_providers.dart b/lib/features/schools/schools_tab_providers.dart index d8c4585..d905165 100644 --- a/lib/features/schools/schools_tab_providers.dart +++ b/lib/features/schools/schools_tab_providers.dart @@ -92,7 +92,7 @@ class FavoriteSchools extends _$FavoriteSchools { class SchoolDivisions extends _$SchoolDivisions { @override Map build() { - final schools = ref.watch(schoolsProvider).value; + final schools = ref.watch(schoolsProvider).valueOrNull; final divisions = schools?.map((e) => e.currentDivision).toSet() ?? {}; return { for (final division in divisions) division: true, @@ -160,12 +160,12 @@ class ShowOnlyFavoriteSchools extends _$ShowOnlyFavoriteSchools { final filteredSchoolsProvider = Provider.autoDispose>((ref) { final filter = ref.watch(schoolDivisionsProvider); - final schools = ref.watch(schoolsProvider); + final schools = ref.watch(schoolsProvider).valueOrNull; final favoritesIds = ref.watch(favoriteSchoolsProvider); final onlyFavorites = ref.watch(showOnlyFavoriteSchoolsProvider); - if (schools.value == null) return ImmutableList(const []); + if (schools == null) return ImmutableList(const []); return ImmutableList([ - for (final school in schools.value!) + for (final school in schools) if (filter[school.currentDivision]! && (!onlyFavorites || favoritesIds.contains('${school.id}'))) school, diff --git a/lib/features/schools/widgets/school_card.dart b/lib/features/schools/widgets/school_card.dart index 248264d..e0a28c1 100644 --- a/lib/features/schools/widgets/school_card.dart +++ b/lib/features/schools/widgets/school_card.dart @@ -2,11 +2,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; + import '../../../extensions/app_localization_extension.dart'; import '../../../extensions/intl_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; import '../../../extensions/string_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; +import '../../../utils/screen_size.dart'; import '../school.dart'; import '../school_extensions.dart'; import '../schools_tab_providers.dart'; @@ -122,7 +123,7 @@ class SchoolInfoCard extends StatelessWidget { child: !showOriginal ? Text( '${school.translatedName}' - '${context.querySize.isNotSmallNorMedium ? '\n' : ' '}', + '${context.screenSize.isLarge ? '\n' : ' '}', maxLines: 2, style: Theme.of(context).textTheme.titleLarge!.copyWith( color: colorScheme.onSurface, @@ -131,7 +132,7 @@ class SchoolInfoCard extends StatelessWidget { ) : Text( '${school.name}' - '${context.querySize.isNotSmallNorMedium ? '\n' : ' '}', + '${!context.screenSize.isLarge ? '\n' : ' '}', maxLines: 2, style: Theme.of(context).textTheme.titleLarge!.copyWith( color: colorScheme.onSurface, diff --git a/lib/features/schools/widgets/school_filter_chips.dart b/lib/features/schools/widgets/school_filter_chips.dart index e76dc03..00afe00 100644 --- a/lib/features/schools/widgets/school_filter_chips.dart +++ b/lib/features/schools/widgets/school_filter_chips.dart @@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; +import '../../../utils/screen_size.dart'; import '../school_extensions.dart'; import '../schools_tab_providers.dart'; @@ -22,7 +22,7 @@ class SchoolFilterChips extends ConsumerWidget { final selectedDivisions = ref.watch(schoolDivisionsProvider); final padding = MediaQuery.paddingOf(context); return SliverCrossAxisConstrained( - maxCrossAxisExtent: largeScreen, + maxCrossAxisExtent: ScreenSize.lg.value, child: SliverToBoxAdapter( child: SizedBox( height: 64, diff --git a/lib/features/schools/widgets/schools_empty_list.dart b/lib/features/schools/widgets/schools_empty_list.dart new file mode 100644 index 0000000..f044d38 --- /dev/null +++ b/lib/features/schools/widgets/schools_empty_list.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../extensions/app_localization_extension.dart'; +import '../../../extensions/theme_of_context_extension.dart'; +import '../schools_tab_providers.dart'; + +class SchoolsEmptyList extends ConsumerWidget { + const SchoolsEmptyList({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SliverFillRemaining( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (ref.watch(schoolsProvider).value?.isEmpty ?? false) ...[ + const SizedBox(height: 8), + Text( + context.loc.noSchoolsFound, + style: context.textTheme.titleMedium!.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ] else ...[ + const SizedBox(height: 8), + if (ref.watch(favoriteSchoolsProvider).isEmpty) + Text( + context.loc.noFavoriteSchools, + style: context.textTheme.titleMedium!.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ) + else + Text( + context.loc.noFilteredSchools, + style: context.textTheme.titleMedium!.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + CupertinoButton( + onPressed: () { + ref + .read(showOnlyFavoriteSchoolsProvider.notifier) + .turnOffShowFavorites(); + ref.read(schoolDivisionsProvider.notifier).selectAll(); + }, + child: Text(context.loc.resetFilters), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/schools/widgets/schools_tab_body.dart b/lib/features/schools/widgets/schools_tab_body.dart index 222de22..63f6915 100644 --- a/lib/features/schools/widgets/schools_tab_body.dart +++ b/lib/features/schools/widgets/schools_tab_body.dart @@ -1,141 +1,86 @@ import 'package:dynamic_height_grid_view/dynamic_height_grid_view.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; -import '../../../extensions/app_localization_extension.dart'; +import '../../../common_widgets/app_animation_wrapper.dart'; +import '../../../common_widgets/app_async_widget.dart'; import '../../../extensions/js_bottom_padding_extension.dart' if (dart.library.js_interop) '../../../extensions/js_bottom_padding_extension_web.dart'; -import '../../../extensions/media_query_context_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../utils/immutable_list.dart'; +import '../../../utils/screen_size.dart'; +import '../school.dart'; import '../schools_tab_providers.dart'; import 'school_card.dart'; +import 'schools_empty_list.dart'; class SchoolsTabBody extends ConsumerWidget { const SchoolsTabBody({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final schoolsFuture = ref.watch(schoolsProvider); return SliverSafeArea( top: false, - sliver: SliverAnimatedSwitcher( - duration: kThemeAnimationDuration, - child: switch (schoolsFuture) { - AsyncData() => Consumer( - builder: (context, ref, child) { - final schools = ref.watch(filteredSchoolsProvider); - return SliverCrossAxisConstrained( - maxCrossAxisExtent: largeScreen, - child: SliverPadding( - padding: EdgeInsets.only( - left: 16, - right: 16 + rightInset(), - ), - sliver: SliverAnimatedSwitcher( - duration: kThemeAnimationDuration, - child: schools.isEmpty - ? const SchoolsEmptyList() - : SliverDynamicHeightGridView( - itemCount: schools.length, - crossAxisCount: - context.querySize.crossAxisCount(), - builder: (context, index) { - final school = schools[index]; - return ProviderScope( - overrides: [ - currentSchoolProvider.overrideWithValue( - schools.firstWhere( - (item) => item.id == school.id, - ), - ), - ], - child: const SchoolCard(), - ); - }, - ), - ), - ), - ); - }, - ), - AsyncError(:final error) => SliverFillRemaining( - key: const ValueKey('error'), - child: Center( - child: Column( - children: [ - Text( - error.toString(), - style: context.textTheme.titleLarge, - ), - ], + sliver: AppAsyncSliver( + asyncValue: ref.watch(schoolsProvider), + onErrorRetry: () => ref.invalidate(schoolsProvider), + child: (value) => Consumer( + builder: (context, ref, child) { + final schools = ref.watch(filteredSchoolsProvider); + return SliverCrossAxisConstrained( + maxCrossAxisExtent: ScreenSize.lg.value, + child: SliverPadding( + padding: EdgeInsets.only( + left: 16, + right: 16 + rightInset(), + ), + sliver: SliverAnimatedSwitcher( + duration: kThemeAnimationDuration, + child: schools.isEmpty + ? const SchoolsEmptyList() + : SliverSchoolsList(schools: schools), ), ), - ), - AsyncLoading() => const SliverFillRemaining( - key: ValueKey('loading'), - child: Center(child: CircularProgressIndicator.adaptive()), - ), - }, + ); + }, + ), ), ); } } -class SchoolsEmptyList extends ConsumerWidget { - const SchoolsEmptyList({super.key}); +class SliverSchoolsList extends StatelessWidget { + const SliverSchoolsList({ + required this.schools, + super.key, + }); + + final ImmutableList schools; @override - Widget build(BuildContext context, WidgetRef ref) { - return SliverFillRemaining( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (ref.watch(schoolsProvider).value?.isEmpty ?? false) ...[ - const SizedBox(height: 8), - Text( - context.loc.noSchoolsFound, - style: context.textTheme.titleMedium!.copyWith( - color: context.colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - ] else ...[ - const SizedBox(height: 8), - if (ref.watch(favoriteSchoolsProvider).isEmpty) - Text( - context.loc.noFavoriteSchools, - style: context.textTheme.titleMedium!.copyWith( - color: context.colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ) - else - Text( - context.loc.noFilteredSchools, - style: context.textTheme.titleMedium!.copyWith( - color: context.colorScheme.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - CupertinoButton( - onPressed: () { - ref - .read(showOnlyFavoriteSchoolsProvider.notifier) - .turnOffShowFavorites(); - ref.read(schoolDivisionsProvider.notifier).selectAll(); - }, - child: Text(context.loc.resetFilters), - ), - ], + Widget build(BuildContext context) { + return SliverDynamicHeightGridView( + itemCount: schools.length, + crossAxisCount: gridCrossAxisCount(context), + builder: (context, index) { + final school = schools[index]; + return AppAnimationWrapper( + child: ProviderScope( + overrides: [ + currentSchoolProvider.overrideWithValue(school), ], + child: const SchoolCard(), ), - ), - ), + ); + }, ); } + + static int gridCrossAxisCount(BuildContext context) { + return switch (context.screenSize) { + ScreenSize.xs => 1, + ScreenSize.md => 2, + ScreenSize.lg => 3 + }; + } } diff --git a/lib/features/schools/widgets/schools_tab_navbar.dart b/lib/features/schools/widgets/schools_tab_navbar.dart index ab3d534..c334db4 100644 --- a/lib/features/schools/widgets/schools_tab_navbar.dart +++ b/lib/features/schools/widgets/schools_tab_navbar.dart @@ -6,7 +6,7 @@ import '../../../common_widgets/app_cupertino_button.dart'; import '../../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../../extensions/app_localization_extension.dart'; import '../../../extensions/hardcoded_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; +import '../../../utils/screen_size.dart'; class SchoolsTabNavBar extends StatelessWidget { const SchoolsTabNavBar({ @@ -16,11 +16,11 @@ class SchoolsTabNavBar extends StatelessWidget { @override Widget build(BuildContext context) { return SliverCrossAxisConstrained( - maxCrossAxisExtent: largeScreen, + maxCrossAxisExtent: ScreenSize.lg.value, child: AppCupertinoSliverNavigationBar( largeTitle: context.loc.schoolsTitle, leading: PullDownButton( - menuOffset: context.querySize.currentRailWidth, + // menuOffset: context.screenSize.currentRailWidth, itemBuilder: (context) => [ // TODO(hectorAguero): Should get this from the Data PullDownMenuItem.selectable( diff --git a/lib/features/schools/widgets/schools_tab_search_header.dart b/lib/features/schools/widgets/schools_tab_search_header.dart index 2d5e516..d8ef297 100644 --- a/lib/features/schools/widgets/schools_tab_search_header.dart +++ b/lib/features/schools/widgets/schools_tab_search_header.dart @@ -6,8 +6,8 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../../common_widgets/app_cupertino_button.dart'; import '../../../common_widgets/app_web_padding.dart'; import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/media_query_context_extension.dart'; import '../../../extensions/theme_of_context_extension.dart'; +import '../../../utils/screen_size.dart'; import '../../home/widgets/adaptive_navigation_rail.dart'; import '../school.dart'; import '../schools_tab_providers.dart'; @@ -40,7 +40,7 @@ class _SchoolsTabSearchHeaderState top: false, bottom: false, sliver: SliverCrossAxisConstrained( - maxCrossAxisExtent: largeScreen, + maxCrossAxisExtent: ScreenSize.lg.value, child: SliverPadding( padding: const EdgeInsets.only(top: 12, left: 16, right: 16), sliver: SliverCrossAxisGroup( @@ -113,12 +113,12 @@ class _SchoolsTabSearchHeaderState } Rect calculateRect(BuildContext context) { - if (context.querySize.width > largeScreen) { - const left = largeScreen + AdaptiveNavigationRail.largeRailWidth; + if (context.screenSize.isLarge) { + final left = ScreenSize.lg.value + AdaptiveNavigationRail.largeRailWidth; - return const Rect.fromLTWH(left, 100, -48, 48); + return Rect.fromLTWH(left, 100, -48, 48); } - final left = context.querySize.width; + final left = MediaQuery.sizeOf(context).width - 48; return Rect.fromLTWH(left, 100, 48, 48); } diff --git a/lib/initialization_page.dart b/lib/initialization_page.dart index fa1df3f..21cb63c 100644 --- a/lib/initialization_page.dart +++ b/lib/initialization_page.dart @@ -1,17 +1,18 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'core/shared_preferences_provider.dart'; import 'extensions/app_localization_extension.dart'; -import 'extensions/media_query_context_extension.dart'; import 'features/home/widgets/adaptive_navigation_rail.dart'; import 'theme/theme_provider.dart'; import 'utils/immutable_list.dart'; import 'utils/main_logger.dart'; +import 'utils/screen_size.dart'; part 'initialization_page.g.dart'; @@ -60,6 +61,7 @@ class InitializationPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final initProvider = ref.watch(initializationProvider); ref.watch(appThemeModeProvider); + return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: switch (initProvider) { @@ -81,7 +83,7 @@ class AppStartupLoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); + final screenSize = context.screenSize; final padding = MediaQuery.paddingOf(context); return Scaffold( appBar: AppBar( @@ -89,15 +91,15 @@ class AppStartupLoadingWidget extends StatelessWidget { toolbarHeight: kMinInteractiveDimensionCupertino + padding.top.clamp(52, 100), ), - bottomNavigationBar: size.isSmallScreen + bottomNavigationBar: screenSize.isSmall ? null : const SizedBox(height: kMinInteractiveDimensionCupertino), - body: size.isSmallScreen + body: screenSize.isSmall ? const Center(child: CircularProgressIndicator.adaptive()) : Row( children: [ SizedBox( - width: size.isExtraLargeScreen || size.isLargeScreen + width: screenSize.isLarge ? AdaptiveNavigationRail.largeRailWidth : AdaptiveNavigationRail.smallRailWidth, ), diff --git a/lib/localization/app_en.arb b/lib/localization/app_en.arb index c048f7a..844fde5 100644 --- a/lib/localization/app_en.arb +++ b/lib/localization/app_en.arb @@ -5,7 +5,6 @@ "description": "The title of the application" }, "@_MAIN_APP": {}, - "noImage": "No image", "error": "Error", "errorMessage": "An error occurred", diff --git a/lib/main.dart b/lib/main.dart index f3ed1ba..ca3eae6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:country_picker/country_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // ignore:depend_on_referenced_packages @@ -15,6 +16,7 @@ import 'theme/theme_provider.dart'; void main() { usePathUrlStrategy(); + runApp(const ProviderScope(child: MainApp())); } @@ -27,6 +29,17 @@ class MainApp extends ConsumerStatefulWidget { } class _MainAppState extends ConsumerState { + @override + void initState() { + super.initState(); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + ), + ); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } + @override Widget build(BuildContext context) { final router = ref.watch(goRouterProvider); diff --git a/lib/router/go_router.dart b/lib/router/go_router.dart index c6e83f0..53cd927 100644 --- a/lib/router/go_router.dart +++ b/lib/router/go_router.dart @@ -150,7 +150,7 @@ void _scrollTabToTheTop(ScrollController controller) { if (controller.hasClients) { controller.animateTo( 0, - duration: kThemeAnimationDuration, + duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); } diff --git a/lib/utils/screen_size.dart b/lib/utils/screen_size.dart new file mode 100644 index 0000000..49fe54e --- /dev/null +++ b/lib/utils/screen_size.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +/// For mobile in landscape for non scrollable content +const _smallHeight = 500.0; + +const _smallWidth = 600.0; +const _mediumWidth = 900.0; +const _largeWidth = 1200.0; + +enum ScreenSize { + xs, + md, + lg; + + bool get isSmall => this == ScreenSize.xs; + bool get isMedium => this == ScreenSize.md; + bool get isLarge => this == ScreenSize.lg; + + double get value => switch (this) { + (ScreenSize.xs) => _smallWidth, + (ScreenSize.md) => _mediumWidth, + (ScreenSize.lg) => _largeWidth, + }; + + static double get smallHeight => _smallHeight; +} + +extension MediaQueryExtension on BuildContext { + ScreenSize get screenSize { + final width = MediaQuery.sizeOf(this).width; + if (width < ScreenSize.xs.value) return ScreenSize.xs; + if (width < ScreenSize.md.value) return ScreenSize.md; + return ScreenSize.lg; + } +}