Skip to content

Commit

Permalink
feat(opening_hours): update every minute; refactor (#554)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
marfavi authored Nov 22, 2023
1 parent 69db31a commit 9db04b1
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 114 deletions.
18 changes: 15 additions & 3 deletions lib/features/opening_hours/domain/entities/timeslot.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
}
11 changes: 0 additions & 11 deletions lib/features/opening_hours/domain/usecases/check_open_status.dart

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,22 +8,17 @@ part 'opening_hours_state.dart';

class OpeningHoursCubit extends Cubit<OpeningHoursState> {
final GetOpeningHours fetchOpeningHours;
final CheckOpenStatus checkIsOpen;

OpeningHoursCubit({
required this.fetchOpeningHours,
required this.checkIsOpen,
}) : super(const OpeningHoursInitial());
OpeningHoursCubit({required this.fetchOpeningHours})
: super(const OpeningHoursInitial());

Future<void> getOpeninghours() async {
final openingHours = fetchOpeningHours();
final isOpen = checkIsOpen();

emit(
OpeningHoursLoaded(
isOpen: isOpen,
openingHours: openingHours.allOpeningHours,
todaysOpeningHours: openingHours.todaysOpeningHours,
week: openingHours.allOpeningHours,
today: openingHours.todaysOpeningHours,
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ class OpeningHoursInitial extends OpeningHoursState {
}

class OpeningHoursLoaded extends OpeningHoursState {
final Map<int, Timeslot> openingHours;
final Option<Timeslot> todaysOpeningHours;
final bool isOpen;
final Map<int, Timeslot> week;
final Option<Timeslot> today;

const OpeningHoursLoaded({
required this.openingHours,
required this.todaysOpeningHours,
required this.isOpen,
});
const OpeningHoursLoaded({required this.week, required this.today});

@override
List<Object?> get props => [openingHours, todaysOpeningHours, isOpen];
List<Object?> get props => [week, today];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OpeningHoursIndicator> createState() => _OpeningHoursIndicatorState();
}

class _OpeningHoursIndicatorState extends State<OpeningHoursIndicator> {
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<Timeslot> 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<OpeningHoursCubit, OpeningHoursState>(
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),
],
);
},
),
),
);
}
}
7 changes: 1 addition & 6 deletions lib/service_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<OpeningHoursLocalDataSource>(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Either<Failure, bool>>(
const Left(ConnectionFailure()),
Expand All @@ -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,
),
],
);
Expand Down

0 comments on commit 9db04b1

Please sign in to comment.