From 9db04b11c0ff6322740088c24665ae0648220cf4 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:04:22 +0100 Subject: [PATCH] feat(opening_hours): update every minute; refactor (#554) - Fix wrong text representation of opening hours in the indicator. - Add minute-wise update feature to the opening hours indicator. - Remove 'isOpen' check from cubit as this is a widget's job. - Update doc comments in Timeslot entity. --- .../domain/entities/timeslot.dart | 18 +++- .../domain/usecases/check_open_status.dart | 11 --- .../cubit/opening_hours_cubit.dart | 14 +-- .../cubit/opening_hours_state.dart | 13 +-- .../pages/opening_hours_page.dart | 2 +- .../widgets/opening_hours_indicator.dart | 97 ++++++++++++++----- lib/service_locator.dart | 7 +- .../usecases/check_open_status_test.dart | 35 ------- .../cubit/opening_hours_cubit_test.dart | 17 +--- 9 files changed, 100 insertions(+), 114 deletions(-) delete mode 100644 lib/features/opening_hours/domain/usecases/check_open_status.dart delete mode 100644 test/features/opening_hours/domain/usecases/check_open_status_test.dart diff --git a/lib/features/opening_hours/domain/entities/timeslot.dart b/lib/features/opening_hours/domain/entities/timeslot.dart index f5638bed..7149a107 100644 --- a/lib/features/opening_hours/domain/entities/timeslot.dart +++ b/lib/features/opening_hours/domain/entities/timeslot.dart @@ -1,7 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; -/// A timeslot with a start and end time. +/// A range of time with a start and end [TimeOfDay]. +/// +/// To get the text representation of a timeslot, use [format]. class Timeslot extends Equatable { final TimeOfDay startTime; final TimeOfDay endTime; @@ -20,7 +22,7 @@ class Timeslot extends Equatable { /// Operators for comparing [TimeOfDay]s. extension TimeOfDayOperators on TimeOfDay { - /// Returns true if [other] is before [this]. + /// Returns true if [other] is before or equal to [this]. bool operator <=(TimeOfDay other) { if (hour < other.hour) { return true; @@ -31,7 +33,17 @@ extension TimeOfDayOperators on TimeOfDay { } } + /// Checks if [this] is between [start] and [end]. + /// + /// Takes into account that [start] can be after [end] (crossing midnight). + bool isBetween(TimeOfDay start, TimeOfDay end) { + return start <= end + ? start <= this && this <= end + : start <= this || this <= end; + } + + /// Checks if [this] is within [timeslot]. bool isInTimeslot(Timeslot timeslot) { - return timeslot.startTime <= this && this <= timeslot.endTime; + return isBetween(timeslot.startTime, timeslot.endTime); } } diff --git a/lib/features/opening_hours/domain/usecases/check_open_status.dart b/lib/features/opening_hours/domain/usecases/check_open_status.dart deleted file mode 100644 index d1fb6291..00000000 --- a/lib/features/opening_hours/domain/usecases/check_open_status.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:coffeecard/features/opening_hours/domain/repositories/opening_hours_repository.dart'; - -class CheckOpenStatus { - final OpeningHoursRepository repository; - - CheckOpenStatus({required this.repository}); - - bool call() { - return repository.isOpen(); - } -} diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart index 1994911d..480f5e0d 100644 --- a/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; -import 'package:coffeecard/features/opening_hours/domain/usecases/check_open_status.dart'; import 'package:coffeecard/features/opening_hours/domain/usecases/get_opening_hours.dart'; import 'package:equatable/equatable.dart'; import 'package:fpdart/fpdart.dart'; @@ -9,22 +8,17 @@ part 'opening_hours_state.dart'; class OpeningHoursCubit extends Cubit { final GetOpeningHours fetchOpeningHours; - final CheckOpenStatus checkIsOpen; - OpeningHoursCubit({ - required this.fetchOpeningHours, - required this.checkIsOpen, - }) : super(const OpeningHoursInitial()); + OpeningHoursCubit({required this.fetchOpeningHours}) + : super(const OpeningHoursInitial()); Future getOpeninghours() async { final openingHours = fetchOpeningHours(); - final isOpen = checkIsOpen(); emit( OpeningHoursLoaded( - isOpen: isOpen, - openingHours: openingHours.allOpeningHours, - todaysOpeningHours: openingHours.todaysOpeningHours, + week: openingHours.allOpeningHours, + today: openingHours.todaysOpeningHours, ), ); } diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart index 4abb2444..e006e37e 100644 --- a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart @@ -12,16 +12,11 @@ class OpeningHoursInitial extends OpeningHoursState { } class OpeningHoursLoaded extends OpeningHoursState { - final Map openingHours; - final Option todaysOpeningHours; - final bool isOpen; + final Map week; + final Option today; - const OpeningHoursLoaded({ - required this.openingHours, - required this.todaysOpeningHours, - required this.isOpen, - }); + const OpeningHoursLoaded({required this.week, required this.today}); @override - List get props => [openingHours, todaysOpeningHours, isOpen]; + List get props => [week, today]; } diff --git a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart index dfe0016d..371007d7 100644 --- a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart +++ b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart @@ -32,7 +32,7 @@ class OpeningHoursPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _OpeningHoursView(openingHours: state.openingHours), + _OpeningHoursView(openingHours: state.week), const Gap(36), Row( children: [ diff --git a/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart b/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart index e86cfa9d..d6fb4887 100644 --- a/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart +++ b/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart @@ -1,50 +1,95 @@ +import 'dart:async'; + import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/styles/app_colors.dart'; import 'package:coffeecard/core/styles/app_text_styles.dart'; +import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; import 'package:coffeecard/features/opening_hours/presentation/cubit/opening_hours_cubit.dart'; import 'package:coffeecard/features/opening_hours/presentation/widgets/analog_icons.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fpdart/fpdart.dart' show Option, Some; import 'package:gap/gap.dart'; import 'package:intl/intl.dart'; -class OpeningHoursIndicator extends StatelessWidget { +class OpeningHoursIndicator extends StatefulWidget { const OpeningHoursIndicator(); - String get formatCurrentWeekday => DateFormat('EEEE') - .format(DateTime.now()) - .characters - .getRange(0, 3) - .toString(); + @override + State createState() => _OpeningHoursIndicatorState(); +} + +class _OpeningHoursIndicatorState extends State { + late Timer timer; + final refreshDuration = const Duration(minutes: 1); + + @override + void initState() { + super.initState(); + timer = Timer.periodic(refreshDuration, (_) => setState(() {})); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + String get currentWeekday => DateFormat.E().format(DateTime.now()); + + (Color, String) openColorAndText(Timeslot timeslot) => ( + AppColors.success, + '$currentWeekday: ${timeslot.format(context)}', + ); + (Color, String) get closedColorAndText => ( + AppColors.errorOnBright, + '${Strings.openingHoursIndicatorPrefix} ${Strings.closed}', + ); + + (Color, String) colorAndText(Option todaysOpeningHours) { + final isOpen = TimeOfDay.now().isInTimeslot; + return switch (todaysOpeningHours) { + Some(value: final hours) when isOpen(hours) => openColorAndText(hours), + _ => closedColorAndText, + }; + } @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, state) { - if (state is! OpeningHoursLoaded) { - return const SizedBox.shrink(); - } + builder: (_, state) { + return switch (state) { + OpeningHoursLoaded(:final today) => _Content(colorAndText(today)), + _ => const SizedBox.shrink(), + }; + }, + ); + } +} - final text = state.isOpen - ? '$formatCurrentWeekday: ${state.todaysOpeningHours}' - : '${Strings.openingHoursIndicatorPrefix} ${Strings.closed}'; - final color = - state.isOpen ? AppColors.success : AppColors.errorOnBright; +class _Content extends StatelessWidget { + _Content((Color, String) colorAndText) + : color = colorAndText.$1, + text = colorAndText.$2; - final textStyle = - AppTextStyle.openingHoursIndicator.copyWith(color: color); + final Color color; + final String text; - return Row( + @override + Widget build(BuildContext context) { + return DefaultTextStyle( + style: AppTextStyle.openingHoursIndicator.copyWith(color: color), + child: Theme( + data: Theme.of(context) + .copyWith(iconTheme: IconThemeData(color: color, size: 18)), + child: Row( children: [ - Icon(AnalogIcons.closed, size: 18, color: color), + const Icon(AnalogIcons.closed), const Gap(8), - Text( - text, - style: textStyle, - ), + Text(text), ], - ); - }, + ), + ), ); } } diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 3b1dd671..38ab4e69 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -31,7 +31,6 @@ import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cub import 'package:coffeecard/features/opening_hours/data/datasources/opening_hours_local_data_source.dart'; import 'package:coffeecard/features/opening_hours/data/repositories/opening_hours_repository_impl.dart'; import 'package:coffeecard/features/opening_hours/domain/repositories/opening_hours_repository.dart'; -import 'package:coffeecard/features/opening_hours/domain/usecases/check_open_status.dart'; import 'package:coffeecard/features/opening_hours/domain/usecases/get_opening_hours.dart'; import 'package:coffeecard/features/opening_hours/presentation/cubit/opening_hours_cubit.dart'; import 'package:coffeecard/features/product/data/datasources/product_remote_data_source.dart'; @@ -151,15 +150,11 @@ void initAuthentication() { void initOpeningHours() { // bloc sl.registerFactory( - () => OpeningHoursCubit( - fetchOpeningHours: sl(), - checkIsOpen: sl(), - ), + () => OpeningHoursCubit(fetchOpeningHours: sl()), ); // use case sl.registerFactory(() => GetOpeningHours(repository: sl())); - sl.registerFactory(() => CheckOpenStatus(repository: sl())); // data source sl.registerLazySingleton( diff --git a/test/features/opening_hours/domain/usecases/check_open_status_test.dart b/test/features/opening_hours/domain/usecases/check_open_status_test.dart deleted file mode 100644 index 83ab944f..00000000 --- a/test/features/opening_hours/domain/usecases/check_open_status_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/features/opening_hours/domain/repositories/opening_hours_repository.dart'; -import 'package:coffeecard/features/opening_hours/domain/usecases/check_open_status.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'check_open_status_test.mocks.dart'; - -@GenerateMocks([OpeningHoursRepository]) -void main() { - late OpeningHoursRepository repository; - late CheckOpenStatus getIsOpen; - - setUp(() { - repository = MockOpeningHoursRepository(); - getIsOpen = CheckOpenStatus(repository: repository); - - provideDummy>( - const Left(ConnectionFailure()), - ); - }); - - test('should call data source', () async { - // arrange - when(repository.isOpen()).thenReturn(true); - - // act - getIsOpen(); - - // assert - verify(repository.isOpen()); - }); -} diff --git a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart index 3c9596d4..1b34abb9 100644 --- a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart +++ b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart @@ -2,7 +2,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; -import 'package:coffeecard/features/opening_hours/domain/usecases/check_open_status.dart'; import 'package:coffeecard/features/opening_hours/domain/usecases/get_opening_hours.dart'; import 'package:coffeecard/features/opening_hours/presentation/cubit/opening_hours_cubit.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,19 +11,14 @@ import 'package:mockito/mockito.dart'; import 'opening_hours_cubit_test.mocks.dart'; -@GenerateMocks([GetOpeningHours, CheckOpenStatus]) +@GenerateMocks([GetOpeningHours]) void main() { late MockGetOpeningHours fetchOpeningHours; - late MockCheckOpenStatus checkIsOpen; late OpeningHoursCubit cubit; setUp(() { fetchOpeningHours = MockGetOpeningHours(); - checkIsOpen = MockCheckOpenStatus(); - cubit = OpeningHoursCubit( - checkIsOpen: checkIsOpen, - fetchOpeningHours: fetchOpeningHours, - ); + cubit = OpeningHoursCubit(fetchOpeningHours: fetchOpeningHours); provideDummy>( const Left(ConnectionFailure()), @@ -39,21 +33,18 @@ void main() { allOpeningHours: {}, todaysOpeningHours: Option.none(), ); - const isOpen = true; blocTest( 'should emit [OpeningHoursLoaded]', build: () => cubit, setUp: () { when(fetchOpeningHours.call()).thenAnswer((_) => theOpeningHours); - when(checkIsOpen.call()).thenAnswer((_) => isOpen); }, act: (_) => cubit.getOpeninghours(), expect: () => [ OpeningHoursLoaded( - openingHours: theOpeningHours.allOpeningHours, - todaysOpeningHours: theOpeningHours.todaysOpeningHours, - isOpen: isOpen, + week: theOpeningHours.allOpeningHours, + today: theOpeningHours.todaysOpeningHours, ), ], );