Skip to content

Commit 42abd90

Browse files
committed
feat: add sudoku timer, pause-resume functionality
1 parent 24abca5 commit 42abd90

File tree

14 files changed

+404
-37
lines changed

14 files changed

+404
-37
lines changed

lib/app/view/app.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class App extends StatelessWidget {
1111
return MaterialApp(
1212
theme: SudokuTheme.light,
1313
darkTheme: SudokuTheme.dark,
14-
themeMode: ThemeMode.light,
1514
localizationsDelegates: AppLocalizations.localizationsDelegates,
1615
supportedLocales: AppLocalizations.supportedLocales,
1716
home: const SudokuPage(),

lib/sudoku/view/sudoku_page.dart

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ import 'package:sudoku/l10n/l10n.dart';
44
import 'package:sudoku/layout/layout.dart';
55
import 'package:sudoku/models/models.dart';
66
import 'package:sudoku/sudoku/sudoku.dart';
7+
import 'package:sudoku/timer/timer.dart';
78
import 'package:sudoku/typography/typography.dart';
89

10+
/// {@template sudoku_page}
11+
/// The root page of the Sudoku UI.
12+
/// {@endtemplate}
913
class SudokuPage extends StatelessWidget {
14+
/// {@macro sudoku_page}
1015
const SudokuPage({super.key});
1116

1217
static const _generated = [
@@ -35,16 +40,29 @@ class SudokuPage extends StatelessWidget {
3540

3641
@override
3742
Widget build(BuildContext context) {
38-
return BlocProvider<SudokuBloc>(
39-
create: (context) => SudokuBloc(
40-
sudoku: Sudoku.fromRawData(_generated, _answer),
41-
),
43+
return MultiBlocProvider(
44+
providers: [
45+
BlocProvider<SudokuBloc>(
46+
create: (context) => SudokuBloc(
47+
sudoku: Sudoku.fromRawData(_generated, _answer),
48+
),
49+
),
50+
BlocProvider<TimerBloc>(
51+
create: (context) => TimerBloc(
52+
ticker: const Ticker(),
53+
)..add(const TimerStarted()),
54+
),
55+
],
4256
child: const SudokuView(),
4357
);
4458
}
4559
}
4660

61+
/// {@template sudoku_view}
62+
/// Displays the content for the [SudokuPage].
63+
/// {@endtemplate}
4764
class SudokuView extends StatelessWidget {
65+
/// {@macro sudoku_view}
4866
const SudokuView({super.key});
4967

5068
@override
@@ -61,9 +79,11 @@ class SudokuView extends StatelessWidget {
6179
body: SingleChildScrollView(
6280
child: Column(
6381
children: [
64-
const ResponsiveGap(
65-
large: 246,
82+
const ResponsiveGap(large: 246),
83+
const Center(
84+
child: SudokuTimer(),
6685
),
86+
const ResponsiveGap(large: 96),
6787
Row(
6888
mainAxisAlignment: MainAxisAlignment.center,
6989
children: [
@@ -76,23 +96,17 @@ class SudokuView extends StatelessWidget {
7696
),
7797
),
7898
),
79-
const SizedBox(
80-
width: 60,
81-
),
99+
const SizedBox(width: 60),
82100
const SudokuBoardView(
83101
layoutSize: ResponsiveLayoutSize.large,
84102
),
85-
const SizedBox(
86-
width: 96,
87-
),
103+
const SizedBox(width: 96),
88104
SudokuInput(
89105
sudokuDimension: sudoku.getDimesion(),
90106
),
91107
],
92108
),
93-
const ResponsiveGap(
94-
large: 246,
95-
),
109+
const ResponsiveGap(large: 246),
96110
],
97111
),
98112
),
@@ -103,25 +117,20 @@ class SudokuView extends StatelessWidget {
103117
title: Text(l10n.sudokuAppBarTitle),
104118
),
105119
body: SingleChildScrollView(
106-
child: Column(
107-
children: [
108-
const ResponsiveGap(
109-
small: 24,
110-
medium: 32,
111-
),
112-
Center(
113-
child: SudokuBoardView(layoutSize: layoutSize),
114-
),
115-
const ResponsiveGap(
116-
small: 32,
117-
medium: 56,
118-
),
119-
Center(
120-
child: SudokuInput(
120+
child: SizedBox(
121+
width: double.maxFinite,
122+
child: Column(
123+
children: [
124+
const ResponsiveGap(small: 16, medium: 24),
125+
const SudokuTimer(),
126+
const ResponsiveGap(small: 16, medium: 24),
127+
SudokuBoardView(layoutSize: layoutSize),
128+
const ResponsiveGap(small: 32, medium: 56),
129+
SudokuInput(
121130
sudokuDimension: sudoku.getDimesion(),
122131
),
123-
),
124-
],
132+
],
133+
),
125134
),
126135
),
127136
);

lib/sudoku/widgets/sudoku_board.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import 'dart:math';
22

33
import 'package:flutter/material.dart';
4+
import 'package:flutter_bloc/flutter_bloc.dart';
45
import 'package:sudoku/layout/layout.dart';
56
import 'package:sudoku/sudoku/sudoku.dart';
7+
import 'package:sudoku/timer/timer.dart';
68

79
/// {@template sudoku_board}
810
/// Displays the Sudoku board in a [Stack] containing [blocks].
11+
///
12+
/// When the timer is paused, it shows a paused icon, and not
13+
/// the [blocks] and its values.
914
/// {@endtemplate}
1015
class SudokuBoard extends StatelessWidget {
1116
/// {@macro sudoku_board}
@@ -16,6 +21,10 @@ class SudokuBoard extends StatelessWidget {
1621

1722
@override
1823
Widget build(BuildContext context) {
24+
final isTimerPaused = context.select(
25+
(TimerBloc bloc) => !bloc.state.isRunning,
26+
);
27+
1928
return ResponsiveLayoutBuilder(
2029
small: (_, child) => SizedBox.square(
2130
key: const Key('sudoku_board_small'),
@@ -46,7 +55,7 @@ class SudokuBoard extends StatelessWidget {
4655
final subGridSize = subGridDimension * blockSize;
4756
return Stack(
4857
children: [
49-
...blocks,
58+
if (!isTimerPaused) ...blocks,
5059
IgnorePointer(
5160
child: SudokuBoardDivider(
5261
dimension: boardSize,
@@ -64,6 +73,16 @@ class SudokuBoard extends StatelessWidget {
6473
),
6574
),
6675
),
76+
if (isTimerPaused)
77+
Center(
78+
child: FloatingActionButton.extended(
79+
onPressed: () => context.read<TimerBloc>().add(
80+
const TimerResumed(),
81+
),
82+
label: const Text('Resume the puzzle'),
83+
icon: const Icon(Icons.play_arrow),
84+
),
85+
),
6786
],
6887
);
6988
},

lib/sudoku/widgets/sudoku_timer.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:sudoku/layout/layout.dart';
4+
import 'package:sudoku/timer/timer.dart';
5+
import 'package:sudoku/typography/typography.dart';
6+
7+
class SudokuTimer extends StatelessWidget {
8+
const SudokuTimer({super.key});
9+
10+
@override
11+
Widget build(BuildContext context) {
12+
final theme = Theme.of(context);
13+
14+
return BlocBuilder<TimerBloc, TimerState>(
15+
builder: (context, state) {
16+
final hour = state.secondsElapsed ~/ 3600;
17+
final minute = (state.secondsElapsed - (hour * 3600)) ~/ 60;
18+
final seconds = state.secondsElapsed - (hour * 3600) - (minute * 60);
19+
20+
final hourString = hour.toString().padLeft(2, '0');
21+
final minuteString = minute.toString().padLeft(2, '0');
22+
final secondsString = seconds.toString().padLeft(2, '0');
23+
24+
return ResponsiveLayoutBuilder(
25+
small: (_, child) => child!,
26+
medium: (_, child) => child!,
27+
large: (_, child) => child!,
28+
child: (layoutSize) {
29+
final timerTextStyle = switch (layoutSize) {
30+
ResponsiveLayoutSize.large => SudokuTextStyle.bodyText1,
31+
_ => SudokuTextStyle.bodyText1,
32+
};
33+
34+
return GestureDetector(
35+
onTap: () => state.isRunning
36+
? context.read<TimerBloc>().add(const TimerStopped())
37+
: context.read<TimerBloc>().add(const TimerResumed()),
38+
child: DecoratedBox(
39+
decoration: BoxDecoration(
40+
border: Border.all(
41+
color: theme.dividerColor,
42+
width: 1.4,
43+
),
44+
borderRadius: BorderRadius.circular(8),
45+
),
46+
child: Padding(
47+
padding: const EdgeInsets.symmetric(
48+
horizontal: 8,
49+
vertical: 2,
50+
),
51+
child: Row(
52+
mainAxisAlignment: MainAxisAlignment.center,
53+
mainAxisSize: MainAxisSize.min,
54+
children: [
55+
Text(
56+
'$hourString:$minuteString:$secondsString',
57+
style: timerTextStyle,
58+
),
59+
Icon(
60+
state.isRunning ? Icons.pause : Icons.play_arrow,
61+
size: timerTextStyle.fontSize,
62+
),
63+
],
64+
),
65+
),
66+
),
67+
);
68+
},
69+
);
70+
},
71+
);
72+
}
73+
}

lib/sudoku/widgets/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export 'sudoku_block.dart';
22
export 'sudoku_board.dart';
33
export 'sudoku_board_divider.dart';
44
export 'sudoku_input.dart';
5+
export 'sudoku_timer.dart';

lib/timer/bloc/timer_bloc.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class TimerBloc extends Bloc<TimerEvent, TimerState> {
1414
on<TimerStarted>(_onTimerStarted);
1515
on<TimerTicked>(_onTimerTicked);
1616
on<TimerStopped>(_onTimerStopped);
17+
on<TimerResumed>(_onTimerResumed);
1718
on<TimerReset>(_onTimerReset);
1819
}
1920

@@ -44,6 +45,11 @@ class TimerBloc extends Bloc<TimerEvent, TimerState> {
4445
emit(state.copyWith(isRunning: false));
4546
}
4647

48+
void _onTimerResumed(TimerResumed event, Emitter<TimerState> emit) {
49+
_tickerSubscription?.resume();
50+
emit(state.copyWith(isRunning: true));
51+
}
52+
4753
void _onTimerReset(TimerReset event, Emitter<TimerState> emit) {
4854
_tickerSubscription?.cancel();
4955
emit(state.copyWith(secondsElapsed: 0));

lib/timer/bloc/timer_event.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ final class TimerStopped extends TimerEvent {
2424
const TimerStopped();
2525
}
2626

27+
final class TimerResumed extends TimerEvent {
28+
const TimerResumed();
29+
}
30+
2731
final class TimerReset extends TimerEvent {
2832
const TimerReset();
2933
}

test/helpers/mocks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
22
import 'package:mocktail/mocktail.dart';
33
import 'package:sudoku/models/models.dart';
44
import 'package:sudoku/sudoku/sudoku.dart';
5+
import 'package:sudoku/timer/timer.dart';
56

67
class MockSudoku extends Mock implements Sudoku {}
78

@@ -13,3 +14,6 @@ class MockSudokuState extends Mock implements SudokuState {}
1314
class MockBlock extends Mock implements Block {}
1415

1516
class MockTicker extends Mock implements Ticker {}
17+
18+
class MockTimerBloc extends MockBloc<TimerEvent, TimerState>
19+
implements TimerBloc {}

test/helpers/pump_app.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,26 @@ import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:flutter_test/flutter_test.dart';
44
import 'package:sudoku/l10n/l10n.dart';
55
import 'package:sudoku/sudoku/sudoku.dart';
6+
import 'package:sudoku/timer/timer.dart';
67

78
import 'helpers.dart';
89

910
extension PumpApp on WidgetTester {
10-
Future<void> pumpApp(Widget widget, {SudokuBloc? sudokuBloc}) {
11+
Future<void> pumpApp(
12+
Widget widget, {
13+
SudokuBloc? sudokuBloc,
14+
TimerBloc? timerBloc,
15+
}) {
1116
return pumpWidget(
12-
BlocProvider<SudokuBloc>.value(
13-
value: sudokuBloc ?? MockSudokuBloc(),
17+
MultiBlocProvider(
18+
providers: [
19+
BlocProvider<SudokuBloc>.value(
20+
value: sudokuBloc ?? MockSudokuBloc(),
21+
),
22+
BlocProvider.value(
23+
value: timerBloc ?? MockTimerBloc(),
24+
),
25+
],
1426
child: MaterialApp(
1527
localizationsDelegates: AppLocalizations.localizationsDelegates,
1628
supportedLocales: AppLocalizations.supportedLocales,

0 commit comments

Comments
 (0)