Skip to content

Commit

Permalink
feat: fetch crossword from firestore (#32)
Browse files Browse the repository at this point in the history
* feat: use crossword repository in bloc

* test: add app and bloc tests

* feat: remove initial section request

* feat: remove unused state and fix tests

* test: fix tests and fix section positioning

* chore: remove unused code

* fix: fix words highlight

* test: fix highlight test

* test: fix crossword_game_tests

* test: fix bloc tests

* test: update test board

* fix: fix crossword file for horizontal words

* chore: left print

* test: add hightlight word changes test

* feat: pr suggestions

* chore: analysis

* feat: use Axis in board generator

* feat: update in board generator instead of section creation

* chore: put back enum in create section
  • Loading branch information
B0berman authored Mar 11, 2024
1 parent 3dd265b commit 2dd5719
Show file tree
Hide file tree
Showing 26 changed files with 457 additions and 609 deletions.
143 changes: 1 addition & 142 deletions assets/test/test_board.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import 'package:crossword_repository/crossword_repository.dart';
import 'package:flame/cache.dart';
import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_crossword/l10n/l10n.dart';
import 'package:io_crossword/loading/loading.dart';
import 'package:provider/provider.dart';

class App extends StatelessWidget {
const App({super.key});
const App({required this.crosswordRepository, super.key});

final CrosswordRepository crosswordRepository;

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
return MultiProvider(
providers: [
Provider.value(value: crosswordRepository),
BlocProvider(
create: (_) => PreloadCubit(
Images(prefix: ''),
Expand Down
3 changes: 3 additions & 0 deletions lib/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';

import 'package:bloc/bloc.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/widgets.dart';

Expand All @@ -22,6 +23,7 @@ class AppBlocObserver extends BlocObserver {
}

typedef BootstrapBuilder = FutureOr<Widget> Function(
FirebaseFirestore firestore,
FirebaseAuth firebaseAuth,
);

Expand All @@ -36,6 +38,7 @@ Future<void> bootstrap(BootstrapBuilder builder) async {

runApp(
await builder(
FirebaseFirestore.instance,
FirebaseAuth.instance,
),
);
Expand Down
120 changes: 27 additions & 93 deletions lib/crossword/bloc/crossword_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,113 +1,47 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:game_domain/game_domain.dart';

part 'crossword_event.dart';
part 'crossword_state.dart';

var _id = 0;
int _defaultIdGenerator() => _id++;

class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
CrosswordBloc({
int Function()? idGenerator,
}) : _idGenerator = idGenerator ?? _defaultIdGenerator,
super(const CrosswordInitial()) {
on<InitialBoardLoadRequested>(_onInitialBoardLoadRequested);
CrosswordBloc(this.crosswordRepository) : super(const CrosswordInitial()) {
on<BoardSectionRequested>(_onBoardSectionRequested);
on<WordSelected>(_onWordSelected);
}

// TODO(any): Replace with real data
final int Function() _idGenerator;

Future<void> _onInitialBoardLoadRequested(
InitialBoardLoadRequested event,
Emitter<CrosswordState> emit,
) async {
final section = BoardSection(
id: '${_idGenerator()}',
position: const Point(2, 2),
size: 40,
words: [
Word(
axis: Axis.horizontal,
position: const Point(0, 0),
answer: 'flutter',
clue: 'flutter',
hints: const ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
],
borderWords: const [],
);

emit(
CrosswordLoaded(
width: 40,
height: 40,
sectionSize: 400,
sections: {
(section.position.x, section.position.y): section,
},
),
);
}
final CrosswordRepository crosswordRepository;

Future<void> _onBoardSectionRequested(
BoardSectionRequested event,
Emitter<CrosswordState> emit,
) async {
final loadedState = state;
if (loadedState is CrosswordLoaded) {
final section = BoardSection(
id: '${_idGenerator()}',
position: Point(event.position.$1, event.position.$2),
size: 40,
words: [
Word(
axis: Axis.horizontal,
position: const Point(0, 0),
answer: 'flutter',
clue: 'flutter',
hints: const ['dart', 'mobile', 'cross-platform'],
visible: false,
solvedTimestamp: null,
),
Word(
axis: Axis.vertical,
position: const Point(8, 3),
answer: 'dino',
clue: 'flutter',
hints: const ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
Word(
position: const Point(4, 6),
axis: Axis.horizontal,
answer: 'sparky',
clue: 'flutter',
hints: const ['dart', 'mobile', 'cross-platform'],
visible: true,
solvedTimestamp: null,
),
],
borderWords: const [],
);

emit(
loadedState.copyWith(
sections: {
...loadedState.sections,
(section.position.x, section.position.y): section,
},
),
);
}
return emit.forEach(
crosswordRepository.watchSectionFromPosition(
event.position.$1,
event.position.$2,
),
onData: (section) {
if (section == null) return state;
final newSection = {
(section.position.x, section.position.y): section,
};

return CrosswordLoaded(
sectionSize: section.size,
sections: state is CrosswordLoaded
? {
...(state as CrosswordLoaded).sections,
...newSection,
}
: {...newSection},
);
},
);
}

FutureOr<void> _onWordSelected(
Expand All @@ -117,8 +51,8 @@ class CrosswordBloc extends Bloc<CrosswordEvent, CrosswordState> {
final currentState = state;
if (currentState is CrosswordLoaded) {
emit(
currentState.withSelectedWord(
WordSelection(
currentState.copyWith(
selectedWord: WordSelection(
section: event.section,
wordId: event.wordId,
),
Expand Down
7 changes: 0 additions & 7 deletions lib/crossword/bloc/crossword_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@ sealed class CrosswordEvent extends Equatable {
const CrosswordEvent();
}

class InitialBoardLoadRequested extends CrosswordEvent {
const InitialBoardLoadRequested();

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

class BoardSectionRequested extends CrosswordEvent {
const BoardSectionRequested(this.position);

Expand Down
22 changes: 5 additions & 17 deletions lib/crossword/bloc/crossword_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ sealed class CrosswordState extends Equatable {

class CrosswordInitial extends CrosswordState {
const CrosswordInitial();
@override
List<Object> get props => [];
}

class CrosswordLoading extends CrosswordState {
const CrosswordLoading();
@override
List<Object> get props => [];
}
Expand All @@ -31,10 +26,11 @@ class WordSelection extends Equatable {

class CrosswordLoaded extends CrosswordState {
const CrosswordLoaded({
required this.width,
required this.height,
required this.sectionSize,
required this.sections,
// TODO(any): get configuration from db
this.width = 40,
this.height = 40,
this.selectedWord,
});

Expand All @@ -49,22 +45,14 @@ class CrosswordLoaded extends CrosswordState {
int? height,
int? sectionSize,
Map<(int, int), BoardSection>? sections,
WordSelection? selectedWord,
}) {
return CrosswordLoaded(
width: width ?? this.width,
height: height ?? this.height,
sectionSize: sectionSize ?? this.sectionSize,
sections: sections ?? this.sections,
);
}

CrosswordState withSelectedWord(WordSelection? selectedWord) {
return CrosswordLoaded(
width: width,
height: height,
sectionSize: sectionSize,
sections: sections,
selectedWord: selectedWord,
selectedWord: selectedWord ?? this.selectedWord,
);
}

Expand Down
17 changes: 11 additions & 6 deletions lib/crossword/game/components/section_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ class SectionTapController extends PositionComponent

@override
void onTapUp(TapUpEvent event) {
final localPosition = event.localPosition;
final boardSection = parent._boardSection;

if (boardSection != null) {
final absolutePosition =
boardSection.position * CrosswordGame.cellSize * boardSection.size;
final localPosition = event.localPosition +
Vector2(
absolutePosition.x.toDouble(),
absolutePosition.y.toDouble(),
);
for (final word in boardSection.words) {
final wordLength =
(word.answer.length * CrosswordGame.cellSize).toDouble();
Expand Down Expand Up @@ -210,11 +216,10 @@ class SectionComponent extends PositionComponent

final y =
word.axis == Axis.vertical ? word.position.y + c : word.position.y;
final offset = sectionPosition +
Vector2(
x * CrosswordGame.cellSize.toDouble(),
y * CrosswordGame.cellSize.toDouble(),
);
final offset = Vector2(
x * CrosswordGame.cellSize.toDouble(),
y * CrosswordGame.cellSize.toDouble(),
);

spriteBatch.add(
source: rect,
Expand Down
6 changes: 3 additions & 3 deletions lib/crossword/game/crossword_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class CrosswordGame extends FlameGame with PanDetector {
// TODO(erickzanardo): Use the assets cubit instead
lettersSprite = await images.load('letters.png');

sectionSize = state.sectionSize;
sectionSize = state.sectionSize * cellSize;

totalArea = Size(
(state.width * cellSize).toDouble(),
Expand All @@ -47,8 +47,8 @@ class CrosswordGame extends FlameGame with PanDetector {
..priority = 1
..viewport = (MaxViewport()..anchor = Anchor.topLeft)
..viewfinder.position = Vector2(
totalArea.width / 2,
totalArea.height / 2,
totalArea.width,
totalArea.height,
);

_updateVisibleSections();
Expand Down
10 changes: 4 additions & 6 deletions lib/crossword/view/crossword_page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:crossword_repository/crossword_repository.dart';
import 'package:flame/game.dart' hide Route;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand All @@ -9,8 +10,9 @@ class CrosswordPage extends StatelessWidget {
static Route<void> route() {
return MaterialPageRoute<void>(
builder: (_) => BlocProvider(
create: (BuildContext context) =>
CrosswordBloc()..add(const InitialBoardLoadRequested()),
create: (BuildContext context) => CrosswordBloc(
context.read<CrosswordRepository>(),
)..add(const BoardSectionRequested((0, 0))),
child: const CrosswordPage(),
),
);
Expand All @@ -34,10 +36,6 @@ class CrosswordView extends StatelessWidget {
child = const Center(
child: CircularProgressIndicator(),
);
} else if (state is CrosswordLoading) {
child = const Center(
child: CircularProgressIndicator(),
);
} else if (state is CrosswordError) {
child = const Center(child: Text('Error loading crossword'));
} else if (state is CrosswordLoaded) {
Expand Down
7 changes: 5 additions & 2 deletions lib/main_development.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:authentication_repository/authentication_repository.dart';
import 'package:crossword_repository/crossword_repository.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/widgets.dart';
import 'package:io_crossword/app/app.dart';
Expand All @@ -16,15 +17,17 @@ void main() async {

unawaited(
bootstrap(
(firebaseAuth) async {
(firestore, firebaseAuth) async {
final authenticationRepository = AuthenticationRepository(
firebaseAuth: firebaseAuth,
);

await authenticationRepository.signInAnonymously();
await authenticationRepository.idToken.first;

return const App();
return App(
crosswordRepository: CrosswordRepository(db: firestore),
);
},
),
);
Expand Down
Loading

0 comments on commit 2dd5719

Please sign in to comment.