Skip to content

Commit

Permalink
feat: add loading, error dialog, and sudoku page routing
Browse files Browse the repository at this point in the history
  • Loading branch information
thisissandipp committed Jul 18, 2024
1 parent a7fa4d0 commit 6802a95
Show file tree
Hide file tree
Showing 25 changed files with 1,221 additions and 161 deletions.
Binary file added assets/icons/gemini.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions lib/assets/assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ abstract class Assets {

/// Expert Puzzle icon.
static const expertPuzzleIcon = 'assets/icons/expert.png';

/// Gemini icon.
static const geminiIcon = 'assets/icons/gemini.png';
}
67 changes: 67 additions & 0 deletions lib/home/bloc/home_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:sudoku/api/api.dart';
import 'package:sudoku/models/models.dart';

part 'home_event.dart';
part 'home_state.dart';

class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc({
required SudokuAPI apiClient,
}) : _apiClient = apiClient,
super(const HomeState()) {
on<SudokuCreationRequested>(_onSudokuCreationRequested);
}

final SudokuAPI _apiClient;

FutureOr<void> _onSudokuCreationRequested(
SudokuCreationRequested event,
Emitter<HomeState> emit,
) async {
emit(
state.copyWith(
sudoku: () => null,
difficulty: () => event.difficulty,
sudokuCreationStatus: () => SudokuCreationStatus.inProgress,
sudokuCreationError: () => null,
),
);

try {
final sudoku = await _apiClient.createSudoku(
difficulty: event.difficulty,
);
emit(
state.copyWith(
sudoku: () => sudoku,
sudokuCreationStatus: () => SudokuCreationStatus.completed,
),
);
} on SudokuInvalidRawDataException catch (_) {
emit(
state.copyWith(
sudokuCreationStatus: () => SudokuCreationStatus.failed,
sudokuCreationError: () => SudokuCreationErrorType.invalidRawData,
),
);
} on SudokuAPIClientException catch (_) {
emit(
state.copyWith(
sudokuCreationStatus: () => SudokuCreationStatus.failed,
sudokuCreationError: () => SudokuCreationErrorType.apiClient,
),
);
} catch (_) {
emit(
state.copyWith(
sudokuCreationStatus: () => SudokuCreationStatus.failed,
sudokuCreationError: () => SudokuCreationErrorType.unexpected,
),
);
}
}
}
14 changes: 14 additions & 0 deletions lib/home/bloc/home_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
part of 'home_bloc.dart';

sealed class HomeEvent extends Equatable {
const HomeEvent();
}

final class SudokuCreationRequested extends HomeEvent {
const SudokuCreationRequested(this.difficulty);

final Difficulty difficulty;

@override
List<Object?> get props => [difficulty];
}
46 changes: 46 additions & 0 deletions lib/home/bloc/home_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
part of 'home_bloc.dart';

enum SudokuCreationStatus { initial, inProgress, completed, failed }

enum SudokuCreationErrorType { unexpected, invalidRawData, apiClient }

class HomeState extends Equatable {
const HomeState({
this.sudoku,
this.difficulty,
this.sudokuCreationStatus = SudokuCreationStatus.initial,
this.sudokuCreationError,
});

final Sudoku? sudoku;
final Difficulty? difficulty;
final SudokuCreationStatus sudokuCreationStatus;
final SudokuCreationErrorType? sudokuCreationError;

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

HomeState copyWith({
Sudoku? Function()? sudoku,
Difficulty? Function()? difficulty,
SudokuCreationStatus Function()? sudokuCreationStatus,
SudokuCreationErrorType? Function()? sudokuCreationError,
}) {
return HomeState(
sudoku: sudoku != null ? sudoku() : this.sudoku,
difficulty: difficulty != null ? difficulty() : this.difficulty,
sudokuCreationStatus: sudokuCreationStatus != null
? sudokuCreationStatus()
: this.sudokuCreationStatus,
sudokuCreationError: sudokuCreationError != null
? sudokuCreationError()
: this.sudokuCreationError,
);
}
}
1 change: 1 addition & 0 deletions lib/home/home.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'bloc/home_bloc.dart';
export 'view/home_page.dart';
84 changes: 72 additions & 12 deletions lib/home/view/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sudoku/api/api.dart';
import 'package:sudoku/assets/assets.dart';
import 'package:sudoku/home/home.dart';
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/typography/typography.dart';
import 'package:sudoku/widgets/widgets.dart';

Expand All @@ -17,7 +22,12 @@ class HomePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return const HomeView();
return BlocProvider<HomeBloc>(
create: (context) => HomeBloc(
apiClient: context.read<SudokuAPI>(),
),
child: const HomeView(),
);
}
}

Expand All @@ -28,13 +38,55 @@ class HomeView extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);

return Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>(
value: theme.brightness == Brightness.light
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
child: const SudokuBackground(
child: HomeViewLayout(),
return BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.sudokuCreationStatus != c.sudokuCreationStatus,
listener: (context, state) {
if (state.sudokuCreationStatus == SudokuCreationStatus.inProgress) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) {
return SudokuLoadingDialog(
key: const Key('sudoku_loading_dialog'),
difficulty: state.difficulty?.name ?? '',
);
},
);
}

if (state.sudoku != null &&
state.sudokuCreationStatus == SudokuCreationStatus.completed) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SudokuPage(sudoku: state.sudoku!),
),
);
}

if (state.sudokuCreationError != null &&
state.sudokuCreationStatus == SudokuCreationStatus.failed) {
Navigator.pop(context);
showDialog<void>(
context: context,
builder: (context) {
return SudokuFailureDialog(
key: const Key('sudoku_failure_dialog'),
errorType: state.sudokuCreationError!,
);
},
);
}
},
child: Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>(
value: theme.brightness == Brightness.light
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
child: const SudokuBackground(
child: HomeViewLayout(),
),
),
),
);
Expand Down Expand Up @@ -476,31 +528,39 @@ class CreateGameSection extends StatelessWidget {
iconAsset: Assets.easyPuzzleIcon,
title: l10n.createEasyGameTitle,
caption: l10n.createEasyGameCaption,
onButtonPressed: () => log('easy_mode'),
onButtonPressed: () => context.read<HomeBloc>().add(
const SudokuCreationRequested(Difficulty.easy),
),
),
CreateGameSectionItem(
key: const Key('create_game_medium_mode'),
textButtonkey: const Key('create_game_medium_mode_text_button'),
iconAsset: Assets.mediumPuzzleIcon,
title: l10n.createMediumGameTitle,
caption: l10n.createMediumGameCaption,
onButtonPressed: () => log('medium_mode'),
onButtonPressed: () => context.read<HomeBloc>().add(
const SudokuCreationRequested(Difficulty.medium),
),
),
CreateGameSectionItem(
key: const Key('create_game_difficult_mode'),
textButtonkey: const Key('create_game_difficult_mode_text_button'),
iconAsset: Assets.difficultPuzzleIcon,
title: l10n.createDifficultGameTitle,
caption: l10n.createDifficultGameCaption,
onButtonPressed: () => log('difficult_mode'),
onButtonPressed: () => context.read<HomeBloc>().add(
const SudokuCreationRequested(Difficulty.difficult),
),
),
CreateGameSectionItem(
key: const Key('create_game_expert_mode'),
textButtonkey: const Key('create_game_expert_mode_text_button'),
iconAsset: Assets.expertPuzzleIcon,
title: l10n.createExpertGameTitle,
caption: l10n.createExpertGameCaption,
onButtonPressed: () => log('expert_mode'),
onButtonPressed: () => context.read<HomeBloc>().add(
const SudokuCreationRequested(Difficulty.expert),
),
),
];

Expand Down
27 changes: 26 additions & 1 deletion lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,30 @@
"buildSudokuBoardButtonText": "Build Sudoku Board -->",
"@buildSudokuBoardButtonText": {
"description": "Text shown in the TextButton for New Game modes in Home Page"
},
"createSudokuDialogTitle": "Built with Gemini",
"@createSudokuDialogTitle": {
"description": "Text shown in the Create Sudoku dialog in Home Page"
},
"createSudokuDialogSubtitle": "Sudoku is built with Gemini API. Please wait while we are building {difficulty, select, easy{an easy} medium{a medium} difficult{a difficult} expert{an expert} other{}} sudoku puzzle for you!",
"@createSudokuDialogSubtitle": {
"description": "Text shown as subtitle in the Create Sudoku dialog in Home Page",
"placeholders": {
"difficulty": {
"type": "String"
}
}
},
"errorDialogTitle": "Error!",
"@errorDialogTitle": {
"description": "Text shown in the title for the error dialog in Home Page"
},
"errorWrongDataDialogSubtitle": "Sudoku are created with the help of Gemini. Sometimes, there can be an error while generating a puzzle. We are sorry about that. Please try again!",
"@errorWrongDataDialogSubtitle": {
"description": "Text shown as subtitle in the error due to wrong data dialog in Home Page"
},
"errorClientDialogSubtitle": "There has been an error while communicating to the backend service. Please try again. If this issue persists, please try after some time.",
"@errorClientDialogSubtitle": {
"description": "Text shown as subtitle in the error due to client dialog in Home Page"
}
}
}
14 changes: 14 additions & 0 deletions lib/models/difficulty.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Defines the sudoku difficulty level
enum Difficulty {
/// Easy level
easy,

/// Medium level
medium,

/// Difficult level
difficult,

/// Expert level
expert
}
1 change: 1 addition & 0 deletions lib/models/models.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'block.dart';
export 'difficulty.dart';
export 'position.dart';
export 'sudoku.dart';
export 'ticker.dart';
40 changes: 20 additions & 20 deletions lib/models/sudoku.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import 'package:equatable/equatable.dart';
import 'package:sudoku/models/models.dart';

/// Error thrown when raw data validation fails during the
/// `generateFromList` operation.
/// `fromRawData` operation.
class SudokuInvalidRawDataException implements Exception {
const SudokuInvalidRawDataException();
}
Expand All @@ -49,39 +49,39 @@ class Sudoku extends Equatable {

/// Converts raw data to a [Sudoku] model.
///
/// The `generated` list defines the initial state of the puzzle,
/// and the `answer` defines the completed state.
/// The `puzzle` list defines the initial state of the puzzle,
/// and the `solution` defines the completed state.
///
/// Throws a [SudokuInvalidRawDataException] when validation fails
/// on raw data. The validation checks for 3 things -
/// - Whether the generated and asnswer have same number of items.
/// - Whether each item of the generated data has same length as generated.
/// - Whether each item of the answer data has same length as answer.
/// - Whether the puzzle and asnswer have same number of items.
/// - Whether each item of the puzzle data has same length as puzzle.
/// - Whether each item of the solution data has same length as solution.
factory Sudoku.fromRawData(
List<List<int>> generated,
List<List<int>> answer,
List<List<int>> puzzle,
List<List<int>> solution,
) {
// Validate the generated and answer list
final sameLength = generated.length == answer.length;
final sameItemLengthGenerated = generated.every(
(item) => item.length == generated.length,
// Validate the puzzle and solution list
final sameLength = puzzle.length == solution.length;
final sameItemLengthPuzzle = puzzle.every(
(item) => item.length == puzzle.length,
);
final sameItemLengthAnswer = answer.every(
(item) => item.length == answer.length,
final sameItemLengthSolution = solution.every(
(item) => item.length == solution.length,
);
if (!(sameLength && sameItemLengthGenerated && sameItemLengthAnswer)) {
if (!(sameLength && sameItemLengthPuzzle && sameItemLengthSolution)) {
throw const SudokuInvalidRawDataException();
}

// Generate blocks from raw data
final blocks = <Block>[];
for (var i = 0; i < generated.length; i++) {
for (var j = 0; j < generated[i].length; j++) {
final isEmptyBlock = generated[i][j] == -1;
for (var i = 0; i < puzzle.length; i++) {
for (var j = 0; j < puzzle[i].length; j++) {
final isEmptyBlock = puzzle[i][j] == -1;
final block = Block(
position: Position(x: i, y: j),
correctValue: isEmptyBlock ? answer[i][j] : generated[i][j],
currentValue: generated[i][j],
correctValue: isEmptyBlock ? solution[i][j] : puzzle[i][j],
currentValue: puzzle[i][j],
isGenerated: !isEmptyBlock,
);
blocks.add(block);
Expand Down
Loading

0 comments on commit 6802a95

Please sign in to comment.