Skip to content

Commit

Permalink
feat: save and resume unfinished puzzle
Browse files Browse the repository at this point in the history
  • Loading branch information
thisissandipp committed Jul 30, 2024
1 parent a23d280 commit f12f7f4
Show file tree
Hide file tree
Showing 25 changed files with 676 additions and 146 deletions.
Empty file modified android/gradlew
100644 → 100755
Empty file.
7 changes: 7 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS

DEPENDENCIES:
- Flutter (from `Flutter`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)

EXTERNAL SOURCES:
Flutter:
:path: Flutter
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"

SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78

PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796

Expand Down
46 changes: 45 additions & 1 deletion lib/home/bloc/home_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
_puzzleRepository = puzzleRepository,
super(const HomeState()) {
on<SudokuCreationRequested>(_onSudokuCreationRequested);
on<UnfinishedPuzzleSubscriptionRequested>(_onSubscriptionRequested);
on<UnfinishedPuzzleResumed>(_onUnfinishedPuzzleResumed);
}

final SudokuAPI _apiClient;
Expand All @@ -39,7 +41,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
final sudoku = await _apiClient.createSudoku(
difficulty: event.difficulty,
);
_puzzleRepository.storePuzzle(
_puzzleRepository.savePuzzleToCache(
puzzle: Puzzle(sudoku: sudoku, difficulty: event.difficulty),
);
emit(
Expand Down Expand Up @@ -70,4 +72,46 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
);
}
}

FutureOr<void> _onSubscriptionRequested(
UnfinishedPuzzleSubscriptionRequested event,
Emitter<HomeState> emit,
) async {
await emit.forEach(
_puzzleRepository.getPuzzleFromLocalMemory(),
onData: (puzzle) {
return state.copyWith(
unfinishedPuzzle: () => puzzle,
);
},
onError: (_, __) => state.copyWith(
unfinishedPuzzle: () => null,
),
);
}

void _onUnfinishedPuzzleResumed(
UnfinishedPuzzleResumed event,
Emitter<HomeState> emit,
) {
final unfinishedPuzzle = state.unfinishedPuzzle;
if (unfinishedPuzzle == null) return;

emit(
state.copyWith(
sudokuCreationStatus: () => SudokuCreationStatus.inProgress,
),
);
// Save the unfinished puzzle to the local cache, to be picked up by
// PuzzleBloc, and at the same time, remove the unfinished saved puzzle
// from local storage.
_puzzleRepository
..savePuzzleToCache(puzzle: unfinishedPuzzle)
..clearPuzzleInLocalMemory();
emit(
state.copyWith(
sudokuCreationStatus: () => SudokuCreationStatus.completed,
),
);
}
}
11 changes: 11 additions & 0 deletions lib/home/bloc/home_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ part of 'home_bloc.dart';

sealed class HomeEvent extends Equatable {
const HomeEvent();

@override
List<Object?> get props => [];
}

final class SudokuCreationRequested extends HomeEvent {
Expand All @@ -12,3 +15,11 @@ final class SudokuCreationRequested extends HomeEvent {
@override
List<Object?> get props => [difficulty];
}

final class UnfinishedPuzzleSubscriptionRequested extends HomeEvent {
const UnfinishedPuzzleSubscriptionRequested();
}

final class UnfinishedPuzzleResumed extends HomeEvent {
const UnfinishedPuzzleResumed();
}
6 changes: 6 additions & 0 deletions lib/home/bloc/home_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,27 @@ class HomeState extends Equatable {
this.difficulty,
this.sudokuCreationStatus = SudokuCreationStatus.initial,
this.sudokuCreationError,
this.unfinishedPuzzle,
});

final Difficulty? difficulty;
final SudokuCreationStatus sudokuCreationStatus;
final SudokuCreationErrorType? sudokuCreationError;
final Puzzle? unfinishedPuzzle;

@override
List<Object?> get props => [
difficulty,
sudokuCreationStatus,
sudokuCreationError,
unfinishedPuzzle,
];

HomeState copyWith({
Difficulty? Function()? difficulty,
SudokuCreationStatus Function()? sudokuCreationStatus,
SudokuCreationErrorType? Function()? sudokuCreationError,
Puzzle? Function()? unfinishedPuzzle,
}) {
return HomeState(
difficulty: difficulty != null ? difficulty() : this.difficulty,
Expand All @@ -36,6 +40,8 @@ class HomeState extends Equatable {
sudokuCreationError: sudokuCreationError != null
? sudokuCreationError()
: this.sudokuCreationError,
unfinishedPuzzle:
unfinishedPuzzle != null ? unfinishedPuzzle() : this.unfinishedPuzzle,
);
}
}
14 changes: 11 additions & 3 deletions lib/home/view/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class HomePage extends StatelessWidget {
create: (context) => HomeBloc(
apiClient: context.read<SudokuAPI>(),
puzzleRepository: context.read<PuzzleRepository>(),
),
)..add(const UnfinishedPuzzleSubscriptionRequested()),
child: const HomeView(),
);
}
Expand Down Expand Up @@ -278,6 +278,10 @@ class HighlightedSection extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;

final unfinishedPuzzle = context.select(
(HomeBloc bloc) => bloc.state.unfinishedPuzzle,
);

final dailyChallengeWidget = HighlightedSectionItem(
key: const Key('daily_challenge_widget'),
elevatedButtonkey: const Key('daily_challenge_widget_elevated_button'),
Expand All @@ -293,9 +297,13 @@ class HighlightedSection extends StatelessWidget {
elevatedButtonkey: const Key('resume_puzzle_widget_elevated_button'),
iconAsset: Assets.unfinishedPuzzleIcon,
title: l10n.resumeSudokuTitle,
subtitle: l10n.resumeSudokuSubtitle,
subtitle: unfinishedPuzzle != null
? l10n.resumeSudokuSubtitle
: l10n.resumeSudokuNoPuzzleSubtitle,
buttonText: 'Resume',
onButtonPressed: null,
onButtonPressed: unfinishedPuzzle != null
? () => context.read<HomeBloc>().add(const UnfinishedPuzzleResumed())
: null,
);

return ResponsiveLayoutBuilder(
Expand Down
6 changes: 5 additions & 1 deletion lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@
"@resumeSudokuTitle": {
"description": "Title shown in the Resume Sudoku card of the Home Page"
},
"resumeSudokuSubtitle": "All clear! Start a new game.",
"resumeSudokuSubtitle": "Complete your last unsolved puzzle.",
"@resumeSudokuSubtitle": {
"description": "Subtitle shown in the Resume Sudoku card of the Home Page"
},
"resumeSudokuNoPuzzleSubtitle": "All clear! Start a new game.",
"@resumeSudokuNoPuzzleSubtitle": {
"description": "Subtitle shown when no unfinished puzzle in the Resume Sudoku card of the Home Page"
},
"createNewGameHeader": "New Game",
"@createNewGameHeader": {
"description": "Header text shown in the Create New Game section of the Home Page"
Expand Down
16 changes: 15 additions & 1 deletion lib/puzzle/bloc/puzzle_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class PuzzleBloc extends Bloc<PuzzleEvent, PuzzleState> {
on<SudokuInputErased>(_onSudokuInputErased);
on<SudokuHintRequested>(_onSudokuHintRequested);
on<HintInteractioCompleted>(_onHintInteractioCompleted);
on<UnfinishedPuzzleSaveRequested>(_onUnfinishedPuzzleSaveRequested);
}

final PuzzleRepository _puzzleRepository;
Expand All @@ -33,7 +34,7 @@ class PuzzleBloc extends Bloc<PuzzleEvent, PuzzleState> {
PuzzleInitialized event,
Emitter<PuzzleState> emit,
) {
final puzzle = _puzzleRepository.getPuzzle()!;
final puzzle = _puzzleRepository.fetchPuzzleFromCache()!;
emit(state.copyWith(puzzle: () => puzzle));
}

Expand Down Expand Up @@ -195,4 +196,17 @@ class PuzzleBloc extends Bloc<PuzzleEvent, PuzzleState> {
),
);
}

void _onUnfinishedPuzzleSaveRequested(
UnfinishedPuzzleSaveRequested event,
Emitter<PuzzleState> emit,
) {
final puzzle = state.puzzle.copyWith(
totalSecondsElapsed: event.elapsedSeconds,
);
emit(state.copyWith(puzzle: () => puzzle));
unawaited(
_puzzleRepository.storePuzzleInLocalMemory(puzzle: puzzle),
);
}
}
9 changes: 9 additions & 0 deletions lib/puzzle/bloc/puzzle_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ final class SudokuHintRequested extends PuzzleEvent {
final class HintInteractioCompleted extends PuzzleEvent {
const HintInteractioCompleted();
}

final class UnfinishedPuzzleSaveRequested extends PuzzleEvent {
const UnfinishedPuzzleSaveRequested(this.elapsedSeconds);

final int elapsedSeconds;

@override
List<Object> get props => [elapsedSeconds];
}
45 changes: 30 additions & 15 deletions lib/puzzle/view/puzzle_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ class PuzzlePage extends StatelessWidget {
)..add(const PuzzleInitialized()),
),
BlocProvider<TimerBloc>(
create: (context) => TimerBloc(
ticker: const Ticker(),
)..add(const TimerStarted()),
create: (context) {
final puzzle = context.read<PuzzleBloc>().state.puzzle;
return TimerBloc(
ticker: const Ticker(),
)..add(TimerStarted(puzzle.totalSecondsElapsed));
},
),
],
child: const PuzzleView(),
Expand Down Expand Up @@ -68,18 +71,30 @@ class PuzzleView extends StatelessWidget {
);
}
},
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
systemOverlayStyle: theme.brightness == Brightness.light
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
),
body: const SudokuBackground(
child: PuzzleViewLayout(),
child: PopScope(
onPopInvoked: (_) {
final puzzleStatus = context.read<PuzzleBloc>().state.puzzleStatus;
if (puzzleStatus == PuzzleStatus.incomplete) {
context.read<PuzzleBloc>().add(
UnfinishedPuzzleSaveRequested(
context.read<TimerBloc>().state.secondsElapsed,
),
);
}
},
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
systemOverlayStyle: theme.brightness == Brightness.light
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
),
body: const SudokuBackground(
child: PuzzleViewLayout(),
),
),
),
);
Expand Down
16 changes: 12 additions & 4 deletions lib/timer/bloc/timer_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ class TimerBloc extends Bloc<TimerEvent, TimerState> {
}

void _onTimerStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(
state.copyWith(
initialValue: event.initialValue,
secondsElapsed: event.initialValue,
isRunning: true,
),
);
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick()
.listen((secondsElapsed) => add(TimerTicked(secondsElapsed)));
emit(state.copyWith(isRunning: true));
_tickerSubscription = _ticker.tick().listen(
(secondsElapsed) => add(
TimerTicked(state.initialValue + secondsElapsed),
),
);
}

void _onTimerTicked(TimerTicked event, Emitter<TimerState> emit) {
Expand Down
7 changes: 6 additions & 1 deletion lib/timer/bloc/timer_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ sealed class TimerEvent extends Equatable {
}

final class TimerStarted extends TimerEvent {
const TimerStarted();
const TimerStarted(this.initialValue);

final int initialValue;

@override
List<Object> get props => [initialValue];
}

final class TimerTicked extends TimerEvent {
Expand Down
6 changes: 5 additions & 1 deletion lib/timer/bloc/timer_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ part of 'timer_bloc.dart';
class TimerState extends Equatable {
const TimerState({
this.isRunning = false,
this.initialValue = 0,
this.secondsElapsed = 0,
});

final bool isRunning;
final int initialValue;
final int secondsElapsed;

@override
List<Object> get props => [isRunning, secondsElapsed];
List<Object> get props => [isRunning, initialValue, secondsElapsed];

TimerState copyWith({
bool? isRunning,
int? initialValue,
int? secondsElapsed,
}) {
return TimerState(
isRunning: isRunning ?? this.isRunning,
initialValue: initialValue ?? this.initialValue,
secondsElapsed: secondsElapsed ?? this.secondsElapsed,
);
}
Expand Down
Loading

0 comments on commit f12f7f4

Please sign in to comment.