Skip to content

Commit

Permalink
feat: add sudoku timer, pause-resume functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
thisissandipp committed Jul 7, 2024
1 parent 24abca5 commit 42abd90
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 37 deletions.
1 change: 0 additions & 1 deletion lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class App extends StatelessWidget {
return MaterialApp(
theme: SudokuTheme.light,
darkTheme: SudokuTheme.dark,
themeMode: ThemeMode.light,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const SudokuPage(),
Expand Down
73 changes: 41 additions & 32 deletions lib/sudoku/view/sudoku_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import 'package:sudoku/l10n/l10n.dart';
import 'package:sudoku/layout/layout.dart';
import 'package:sudoku/models/models.dart';
import 'package:sudoku/sudoku/sudoku.dart';
import 'package:sudoku/timer/timer.dart';
import 'package:sudoku/typography/typography.dart';

/// {@template sudoku_page}
/// The root page of the Sudoku UI.
/// {@endtemplate}
class SudokuPage extends StatelessWidget {
/// {@macro sudoku_page}
const SudokuPage({super.key});

static const _generated = [
Expand Down Expand Up @@ -35,16 +40,29 @@ class SudokuPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return BlocProvider<SudokuBloc>(
create: (context) => SudokuBloc(
sudoku: Sudoku.fromRawData(_generated, _answer),
),
return MultiBlocProvider(
providers: [
BlocProvider<SudokuBloc>(
create: (context) => SudokuBloc(
sudoku: Sudoku.fromRawData(_generated, _answer),
),
),
BlocProvider<TimerBloc>(
create: (context) => TimerBloc(
ticker: const Ticker(),
)..add(const TimerStarted()),
),
],
child: const SudokuView(),
);
}
}

/// {@template sudoku_view}
/// Displays the content for the [SudokuPage].
/// {@endtemplate}
class SudokuView extends StatelessWidget {
/// {@macro sudoku_view}
const SudokuView({super.key});

@override
Expand All @@ -61,9 +79,11 @@ class SudokuView extends StatelessWidget {
body: SingleChildScrollView(
child: Column(
children: [
const ResponsiveGap(
large: 246,
const ResponsiveGap(large: 246),
const Center(
child: SudokuTimer(),
),
const ResponsiveGap(large: 96),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expand All @@ -76,23 +96,17 @@ class SudokuView extends StatelessWidget {
),
),
),
const SizedBox(
width: 60,
),
const SizedBox(width: 60),
const SudokuBoardView(
layoutSize: ResponsiveLayoutSize.large,
),
const SizedBox(
width: 96,
),
const SizedBox(width: 96),
SudokuInput(
sudokuDimension: sudoku.getDimesion(),
),
],
),
const ResponsiveGap(
large: 246,
),
const ResponsiveGap(large: 246),
],
),
),
Expand All @@ -103,25 +117,20 @@ class SudokuView extends StatelessWidget {
title: Text(l10n.sudokuAppBarTitle),
),
body: SingleChildScrollView(
child: Column(
children: [
const ResponsiveGap(
small: 24,
medium: 32,
),
Center(
child: SudokuBoardView(layoutSize: layoutSize),
),
const ResponsiveGap(
small: 32,
medium: 56,
),
Center(
child: SudokuInput(
child: SizedBox(
width: double.maxFinite,
child: Column(
children: [
const ResponsiveGap(small: 16, medium: 24),
const SudokuTimer(),
const ResponsiveGap(small: 16, medium: 24),
SudokuBoardView(layoutSize: layoutSize),
const ResponsiveGap(small: 32, medium: 56),
SudokuInput(
sudokuDimension: sudoku.getDimesion(),
),
),
],
],
),
),
),
);
Expand Down
21 changes: 20 additions & 1 deletion lib/sudoku/widgets/sudoku_board.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sudoku/layout/layout.dart';
import 'package:sudoku/sudoku/sudoku.dart';
import 'package:sudoku/timer/timer.dart';

/// {@template sudoku_board}
/// Displays the Sudoku board in a [Stack] containing [blocks].
///
/// When the timer is paused, it shows a paused icon, and not
/// the [blocks] and its values.
/// {@endtemplate}
class SudokuBoard extends StatelessWidget {
/// {@macro sudoku_board}
Expand All @@ -16,6 +21,10 @@ class SudokuBoard extends StatelessWidget {

@override
Widget build(BuildContext context) {
final isTimerPaused = context.select(
(TimerBloc bloc) => !bloc.state.isRunning,
);

return ResponsiveLayoutBuilder(
small: (_, child) => SizedBox.square(
key: const Key('sudoku_board_small'),
Expand Down Expand Up @@ -46,7 +55,7 @@ class SudokuBoard extends StatelessWidget {
final subGridSize = subGridDimension * blockSize;
return Stack(
children: [
...blocks,
if (!isTimerPaused) ...blocks,
IgnorePointer(
child: SudokuBoardDivider(
dimension: boardSize,
Expand All @@ -64,6 +73,16 @@ class SudokuBoard extends StatelessWidget {
),
),
),
if (isTimerPaused)
Center(
child: FloatingActionButton.extended(
onPressed: () => context.read<TimerBloc>().add(
const TimerResumed(),
),
label: const Text('Resume the puzzle'),
icon: const Icon(Icons.play_arrow),
),
),
],
);
},
Expand Down
73 changes: 73 additions & 0 deletions lib/sudoku/widgets/sudoku_timer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sudoku/layout/layout.dart';
import 'package:sudoku/timer/timer.dart';
import 'package:sudoku/typography/typography.dart';

class SudokuTimer extends StatelessWidget {
const SudokuTimer({super.key});

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

return BlocBuilder<TimerBloc, TimerState>(
builder: (context, state) {
final hour = state.secondsElapsed ~/ 3600;
final minute = (state.secondsElapsed - (hour * 3600)) ~/ 60;
final seconds = state.secondsElapsed - (hour * 3600) - (minute * 60);

final hourString = hour.toString().padLeft(2, '0');
final minuteString = minute.toString().padLeft(2, '0');
final secondsString = seconds.toString().padLeft(2, '0');

return ResponsiveLayoutBuilder(
small: (_, child) => child!,
medium: (_, child) => child!,
large: (_, child) => child!,
child: (layoutSize) {
final timerTextStyle = switch (layoutSize) {
ResponsiveLayoutSize.large => SudokuTextStyle.bodyText1,
_ => SudokuTextStyle.bodyText1,
};

return GestureDetector(
onTap: () => state.isRunning
? context.read<TimerBloc>().add(const TimerStopped())
: context.read<TimerBloc>().add(const TimerResumed()),
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor,
width: 1.4,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$hourString:$minuteString:$secondsString',
style: timerTextStyle,
),
Icon(
state.isRunning ? Icons.pause : Icons.play_arrow,
size: timerTextStyle.fontSize,
),
],
),
),
),
);
},
);
},
);
}
}
1 change: 1 addition & 0 deletions lib/sudoku/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export 'sudoku_block.dart';
export 'sudoku_board.dart';
export 'sudoku_board_divider.dart';
export 'sudoku_input.dart';
export 'sudoku_timer.dart';
6 changes: 6 additions & 0 deletions lib/timer/bloc/timer_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class TimerBloc extends Bloc<TimerEvent, TimerState> {
on<TimerStarted>(_onTimerStarted);
on<TimerTicked>(_onTimerTicked);
on<TimerStopped>(_onTimerStopped);
on<TimerResumed>(_onTimerResumed);
on<TimerReset>(_onTimerReset);
}

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

void _onTimerResumed(TimerResumed event, Emitter<TimerState> emit) {
_tickerSubscription?.resume();
emit(state.copyWith(isRunning: true));
}

void _onTimerReset(TimerReset event, Emitter<TimerState> emit) {
_tickerSubscription?.cancel();
emit(state.copyWith(secondsElapsed: 0));
Expand Down
4 changes: 4 additions & 0 deletions lib/timer/bloc/timer_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ final class TimerStopped extends TimerEvent {
const TimerStopped();
}

final class TimerResumed extends TimerEvent {
const TimerResumed();
}

final class TimerReset extends TimerEvent {
const TimerReset();
}
4 changes: 4 additions & 0 deletions test/helpers/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:sudoku/models/models.dart';
import 'package:sudoku/sudoku/sudoku.dart';
import 'package:sudoku/timer/timer.dart';

class MockSudoku extends Mock implements Sudoku {}

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

class MockTicker extends Mock implements Ticker {}

class MockTimerBloc extends MockBloc<TimerEvent, TimerState>
implements TimerBloc {}
18 changes: 15 additions & 3 deletions test/helpers/pump_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,26 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sudoku/l10n/l10n.dart';
import 'package:sudoku/sudoku/sudoku.dart';
import 'package:sudoku/timer/timer.dart';

import 'helpers.dart';

extension PumpApp on WidgetTester {
Future<void> pumpApp(Widget widget, {SudokuBloc? sudokuBloc}) {
Future<void> pumpApp(
Widget widget, {
SudokuBloc? sudokuBloc,
TimerBloc? timerBloc,
}) {
return pumpWidget(
BlocProvider<SudokuBloc>.value(
value: sudokuBloc ?? MockSudokuBloc(),
MultiBlocProvider(
providers: [
BlocProvider<SudokuBloc>.value(
value: sudokuBloc ?? MockSudokuBloc(),
),
BlocProvider.value(
value: timerBloc ?? MockTimerBloc(),
),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
Expand Down
Loading

0 comments on commit 42abd90

Please sign in to comment.