Skip to content

Commit

Permalink
Update clock
Browse files Browse the repository at this point in the history
  • Loading branch information
veloce committed Nov 6, 2024
1 parent 6798e9d commit 547fb9e
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 57 deletions.
5 changes: 5 additions & 0 deletions lib/src/model/game/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,11 @@ class GameController extends _$GameController {
}
}

/// Play a sound when the clock is about to run out
void onClockEmergency() {
ref.read(soundServiceProvider).play(Sound.lowTime);
}

void onFlag() {
_onFlagThrottler(() {
if (state.hasValue) {
Expand Down
8 changes: 6 additions & 2 deletions lib/src/view/game/game_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ class GameBody extends ConsumerWidget {
emergencyThreshold: youAre == Side.black
? gameState.game.meta.clock?.emergency
: null,
emergencySoundEnabled: emergencySoundEnabled,
onEmergency: emergencySoundEnabled
? () => ref.read(ctrlProvider.notifier).onClockEmergency()
: null,
onFlag: () => ref.read(ctrlProvider.notifier).onFlag(),
)
: gameState.game.correspondenceClock != null
Expand Down Expand Up @@ -199,7 +201,9 @@ class GameBody extends ConsumerWidget {
emergencyThreshold: youAre == Side.white
? gameState.game.meta.clock?.emergency
: null,
emergencySoundEnabled: emergencySoundEnabled,
onEmergency: emergencySoundEnabled
? () => ref.read(ctrlProvider.notifier).onClockEmergency()
: null,
onFlag: () => ref.read(ctrlProvider.notifier).onFlag(),
)
: gameState.game.correspondenceClock != null
Expand Down
25 changes: 11 additions & 14 deletions lib/src/widgets/countdown_clock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@ import 'dart:async';
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
import 'package:lichess_mobile/src/utils/screen.dart';

/// A countdown clock.
///
/// The clock starts only when [active] is `true`.
class CountdownClock extends ConsumerStatefulWidget {
class CountdownClock extends StatefulWidget {
const CountdownClock({
required this.timeLeft,
this.delay,
this.clockStartTime,
required this.active,
this.emergencyThreshold,
this.emergencySoundEnabled = true,
this.onEmergency,
this.onFlag,
this.onStop,
this.clockStyle,
Expand All @@ -40,13 +38,13 @@ class CountdownClock extends ConsumerStatefulWidget {
/// event was received from the server and to compensate for UI lag.
final DateTime? clockStartTime;

/// If [timeLeft] is less than [emergencyThreshold], the clock will change
/// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor
/// If [emergencySoundEnabled] is `true`, the clock will also play a sound.
/// The duration at which the clock should change its background color to indicate an emergency.
///
/// If [onEmergency] is provided, the clock will call it when the emergency threshold is reached.
final Duration? emergencyThreshold;

/// Whether to play an emergency sound when the clock reaches the emergency
final bool emergencySoundEnabled;
/// Called when the clock reaches the emergency.
final VoidCallback? onEmergency;

/// If [active] is `true`, the clock starts counting down.
final bool active;
Expand All @@ -64,13 +62,13 @@ class CountdownClock extends ConsumerStatefulWidget {
final bool padLeft;

@override
ConsumerState<CountdownClock> createState() => _CountdownClockState();
State<CountdownClock> createState() => _CountdownClockState();
}

const _emergencyDelay = Duration(seconds: 20);
const _showTenthsThreshold = Duration(seconds: 10);

class _CountdownClockState extends ConsumerState<CountdownClock> {
class _CountdownClockState extends State<CountdownClock> {
Timer? _timer;
Duration timeLeft = Duration.zero;
bool _shouldPlayEmergencyFeedback = true;
Expand Down Expand Up @@ -145,9 +143,7 @@ class _CountdownClockState extends ConsumerState<CountdownClock> {
(_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) {
_shouldPlayEmergencyFeedback = false;
_nextEmergency = clock.now().add(_emergencyDelay);
if (widget.emergencySoundEnabled) {
ref.read(soundServiceProvider).play(Sound.lowTime);
}
widget.onEmergency?.call();
HapticFeedback.heavyImpact();
} else if (widget.emergencyThreshold != null &&
timeLeft > widget.emergencyThreshold! * 1.5) {
Expand All @@ -168,6 +164,7 @@ class _CountdownClockState extends ConsumerState<CountdownClock> {
void didUpdateWidget(CountdownClock oldClock) {
super.didUpdateWidget(oldClock);
final isSameTimeConfig = widget.timeLeft == oldClock.timeLeft;

if (!isSameTimeConfig) {
timeLeft = widget.timeLeft;
}
Expand Down
104 changes: 63 additions & 41 deletions test/widgets/countdown_clock_test.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
import 'package:lichess_mobile/src/widgets/countdown_clock.dart';
import 'package:mocktail/mocktail.dart';

import '../test_provider_scope.dart';

class MockSoundService extends Mock implements SoundService {}

void main() {
setUpAll(() {
registerFallbackValue(MockSoundService());
});

testWidgets('does not tick when not active', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
Expand Down Expand Up @@ -49,7 +39,42 @@ void main() {
expect(find.text('0:00.0', findRichText: true), findsOneWidget);
});

testWidgets('ticks when active', (WidgetTester tester) async {
testWidgets('update time by changing widget configuration',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: CountdownClock(
timeLeft: Duration(seconds: 10),
active: true,
),
),
);

expect(find.text('0:10', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:09.9', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:09.8', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:09.7', findRichText: true), findsOneWidget);

await tester.pumpWidget(
const MaterialApp(
home: CountdownClock(
timeLeft: Duration(seconds: 11),
active: true,
),
),
);
expect(find.text('0:11', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:10', findRichText: true), findsOneWidget);
await tester.pump(const Duration(seconds: 11));
expect(find.text('0:00.0', findRichText: true), findsOneWidget);
});

testWidgets('do not update if timeLeft widget configuration is same',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: CountdownClock(
Expand All @@ -64,6 +89,19 @@ void main() {
expect(find.text('0:09.9', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:09.8', findRichText: true), findsOneWidget);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:09.7', findRichText: true), findsOneWidget);

await tester.pumpWidget(
const MaterialApp(
home: CountdownClock(
timeLeft: Duration(seconds: 10),
active: true,
),
),
);

expect(find.text('0:09.7', findRichText: true), findsOneWidget);
await tester.pump(const Duration(seconds: 10));
expect(find.text('0:00.0', findRichText: true), findsOneWidget);
});
Expand Down Expand Up @@ -244,45 +282,29 @@ void main() {
});

testWidgets('emergency feedback', (WidgetTester tester) async {
final mockSoundService = MockSoundService();
when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {});

const timeLeft = Duration(seconds: 10);

final app = await makeTestProviderScopeApp(
tester,
home: const CountdownClock(
timeLeft: timeLeft,
active: true,
emergencyThreshold: Duration(seconds: 5),
int onEmergencyCount = 0;
await tester.pumpWidget(
MaterialApp(
home: CountdownClock(
timeLeft: const Duration(seconds: 10),
active: true,
emergencyThreshold: const Duration(seconds: 5),
onEmergency: () {
onEmergencyCount++;
},
),
),
overrides: [
soundServiceProvider.overrideWith((ref) => mockSoundService),
],
);
await tester.pumpWidget(app);

expect(find.text('0:10', findRichText: true), findsOneWidget);
await tester.pump(const Duration(seconds: 5));
expect(find.text('0:05.0', findRichText: true), findsOneWidget);
verifyNever(() => mockSoundService.play(Sound.lowTime));
expect(onEmergencyCount, 0);
await tester.pump(const Duration(milliseconds: 1));
verify(() => mockSoundService.play(Sound.lowTime)).called(1);
expect(onEmergencyCount, 1);
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('0:04.8', findRichText: true), findsOneWidget);
// emergency is only called once as long as the time is below the threshold
verifyNever(() => mockSoundService.play(Sound.lowTime));

// notifier.value = const CountdownClock(
// timeLeft: Duration(milliseconds: 5100),
// active: true,
// emergencyThreshold: Duration(seconds: 5),
// );
// await tester.pump();
// expect(find.text('0:05.1', findRichText: true), findsOneWidget);
// await tester.pump(const Duration(milliseconds: 150));
// expect(find.text('0:04.9', findRichText: true), findsOneWidget);
// // emergency is called again after the threshold is passed
// verify(() => mockSoundService.play(Sound.lowTime)).called(1);
expect(onEmergencyCount, 1);
});
}

0 comments on commit 547fb9e

Please sign in to comment.