mirror of https://github.com/flutter/pinball.git
feat: add character selection (#20)
* chore: lock file * feat: character selection page * fix: ignore generated asset coverage * chore: add suggestions * feat: tint ball with theme color * refactor: decrease theme cubit scope * chore: minimize changes * chore: typos and readability * refactor: use extension for initial pinball game * fix: tests from merge * refactor: ignore docs for views * refactor: revert to ignoring for file * fix: todo analyzer warningpull/33/head
parent
07db88d355
commit
ffef053678
@ -1,3 +1,2 @@
|
|||||||
export 'game_hud.dart';
|
|
||||||
export 'pinball_game_page.dart';
|
export 'pinball_game_page.dart';
|
||||||
export 'widgets/widgets.dart';
|
export 'widgets/widgets.dart';
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
export 'game_hud.dart';
|
||||||
export 'game_over_dialog.dart';
|
export 'game_over_dialog.dart';
|
||||||
|
@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "en",
|
"@@locale": "en",
|
||||||
"play": "Play"
|
"play": "Play",
|
||||||
|
"@play": {
|
||||||
|
"description": "Text displayed on the landing page play button"
|
||||||
|
},
|
||||||
|
"start": "Start",
|
||||||
|
"@start": {
|
||||||
|
"description": "Text displayed on the character selection page start button"
|
||||||
|
},
|
||||||
|
"characterSelectionTitle": "Choose your character!",
|
||||||
|
"@characterSelectionTitle": {
|
||||||
|
"description": "Title text displayed on the character selection page"
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "es",
|
"@@locale": "es",
|
||||||
"play": "Jugar"
|
"play": "Jugar",
|
||||||
|
"@play": {
|
||||||
|
"description": "Text displayed on the landing page play button"
|
||||||
|
},
|
||||||
|
"start": "Comienzo",
|
||||||
|
"@start": {
|
||||||
|
"description": "Text displayed on the character selection page start button"
|
||||||
|
},
|
||||||
|
"characterSelectionTitle": "¡Elige a tu personaje!",
|
||||||
|
"@characterSelectionTitle": {
|
||||||
|
"description": "Title text displayed on the character selection page"
|
||||||
|
}
|
||||||
}
|
}
|
@ -1 +1,2 @@
|
|||||||
export 'cubit/theme_cubit.dart';
|
export 'cubit/theme_cubit.dart';
|
||||||
|
export 'view/view.dart';
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball/theme/theme.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
class CharacterSelectionPage extends StatelessWidget {
|
||||||
|
const CharacterSelectionPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static Route route() {
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (_) => const CharacterSelectionPage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => ThemeCubit(),
|
||||||
|
child: const CharacterSelectionView(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CharacterSelectionView extends StatelessWidget {
|
||||||
|
const CharacterSelectionView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
Text(
|
||||||
|
l10n.characterSelectionTitle,
|
||||||
|
style: Theme.of(context).textTheme.headline3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
const _CharacterSelectionGridView(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).push<void>(
|
||||||
|
PinballGamePage.route(
|
||||||
|
theme: context.read<ThemeCubit>().state.theme,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(l10n.start),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CharacterSelectionGridView extends StatelessWidget {
|
||||||
|
const _CharacterSelectionGridView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 20,
|
||||||
|
crossAxisSpacing: 20,
|
||||||
|
children: const [
|
||||||
|
CharacterImageButton(
|
||||||
|
DashTheme(),
|
||||||
|
key: Key('characterSelectionPage_dashButton'),
|
||||||
|
),
|
||||||
|
CharacterImageButton(
|
||||||
|
SparkyTheme(),
|
||||||
|
key: Key('characterSelectionPage_sparkyButton'),
|
||||||
|
),
|
||||||
|
CharacterImageButton(
|
||||||
|
AndroidTheme(),
|
||||||
|
key: Key('characterSelectionPage_androidButton'),
|
||||||
|
),
|
||||||
|
CharacterImageButton(
|
||||||
|
DinoTheme(),
|
||||||
|
key: Key('characterSelectionPage_dinoButton'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(allisonryan0002): remove visibility when adding final UI.
|
||||||
|
@visibleForTesting
|
||||||
|
class CharacterImageButton extends StatelessWidget {
|
||||||
|
const CharacterImageButton(
|
||||||
|
this.characterTheme, {
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final CharacterTheme characterTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final currentCharacterTheme = context.select<ThemeCubit, CharacterTheme>(
|
||||||
|
(cubit) => cubit.state.theme.characterTheme,
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: (currentCharacterTheme == characterTheme)
|
||||||
|
? Colors.blue.withOpacity(0.5)
|
||||||
|
: null,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: characterTheme.characterAsset.image(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export 'character_selection_page.dart';
|
@ -1 +1,4 @@
|
|||||||
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- lib/**/*.gen.dart
|
After Width: | Height: | Size: 274 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 166 KiB |
After Width: | Height: | Size: 223 KiB |
@ -1,4 +1,5 @@
|
|||||||
library pinball_theme;
|
library pinball_theme;
|
||||||
|
|
||||||
|
export 'src/generated/generated.dart';
|
||||||
export 'src/pinball_theme.dart';
|
export 'src/pinball_theme.dart';
|
||||||
export 'src/themes/themes.dart';
|
export 'src/themes/themes.dart';
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
/// *****************************************************
|
||||||
|
/// FlutterGen
|
||||||
|
/// *****************************************************
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class $AssetsImagesGen {
|
||||||
|
const $AssetsImagesGen();
|
||||||
|
|
||||||
|
AssetGenImage get android => const AssetGenImage('assets/images/android.png');
|
||||||
|
AssetGenImage get dash => const AssetGenImage('assets/images/dash.png');
|
||||||
|
AssetGenImage get dino => const AssetGenImage('assets/images/dino.png');
|
||||||
|
AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
class Assets {
|
||||||
|
Assets._();
|
||||||
|
|
||||||
|
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetGenImage extends AssetImage {
|
||||||
|
const AssetGenImage(String assetName)
|
||||||
|
: super(assetName, package: 'pinball_theme');
|
||||||
|
|
||||||
|
Image image({
|
||||||
|
Key? key,
|
||||||
|
ImageFrameBuilder? frameBuilder,
|
||||||
|
ImageLoadingBuilder? loadingBuilder,
|
||||||
|
ImageErrorWidgetBuilder? errorBuilder,
|
||||||
|
String? semanticLabel,
|
||||||
|
bool excludeFromSemantics = false,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
Color? color,
|
||||||
|
BlendMode? colorBlendMode,
|
||||||
|
BoxFit? fit,
|
||||||
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
|
ImageRepeat repeat = ImageRepeat.noRepeat,
|
||||||
|
Rect? centerSlice,
|
||||||
|
bool matchTextDirection = false,
|
||||||
|
bool gaplessPlayback = false,
|
||||||
|
bool isAntiAlias = false,
|
||||||
|
FilterQuality filterQuality = FilterQuality.low,
|
||||||
|
}) {
|
||||||
|
return Image(
|
||||||
|
key: key,
|
||||||
|
image: this,
|
||||||
|
frameBuilder: frameBuilder,
|
||||||
|
loadingBuilder: loadingBuilder,
|
||||||
|
errorBuilder: errorBuilder,
|
||||||
|
semanticLabel: semanticLabel,
|
||||||
|
excludeFromSemantics: excludeFromSemantics,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
color: color,
|
||||||
|
colorBlendMode: colorBlendMode,
|
||||||
|
fit: fit,
|
||||||
|
alignment: alignment,
|
||||||
|
repeat: repeat,
|
||||||
|
centerSlice: centerSlice,
|
||||||
|
matchTextDirection: matchTextDirection,
|
||||||
|
gaplessPlayback: gaplessPlayback,
|
||||||
|
isAntiAlias: isAntiAlias,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get path => assetName;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export 'assets.gen.dart';
|
@ -1,19 +1,28 @@
|
|||||||
import 'package:flame_test/flame_test.dart';
|
import 'package:flame_test/flame_test.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:pinball/game/game.dart';
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
FlameTester<PinballGame> flameBlocTester({
|
FlameTester<PinballGame> flameBlocTester({
|
||||||
required GameBloc Function() gameBlocBuilder,
|
required GameBloc gameBloc,
|
||||||
}) {
|
}) {
|
||||||
return FlameTester<PinballGame>(
|
return FlameTester<PinballGame>(
|
||||||
PinballGame.new,
|
PinballGameX.initial,
|
||||||
pumpWidget: (gameWidget, tester) async {
|
pumpWidget: (gameWidget, tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
BlocProvider.value(
|
BlocProvider.value(
|
||||||
value: gameBlocBuilder(),
|
value: gameBloc,
|
||||||
child: gameWidget,
|
child: gameWidget,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PinballGameX on PinballGame {
|
||||||
|
static PinballGame initial() => PinballGame(
|
||||||
|
theme: const PinballTheme(
|
||||||
|
characterTheme: DashTheme(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -0,0 +1,110 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mockingjay/mockingjay.dart';
|
||||||
|
import 'package:pinball/theme/theme.dart';
|
||||||
|
import 'package:pinball_theme/pinball_theme.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ThemeCubit themeCubit;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
themeCubit = MockThemeCubit();
|
||||||
|
whenListen(
|
||||||
|
themeCubit,
|
||||||
|
const Stream<ThemeState>.empty(),
|
||||||
|
initialState: const ThemeState.initial(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CharacterSelectionPage', () {
|
||||||
|
testWidgets('renders CharacterSelectionView', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
CharacterSelectionPage(),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
);
|
||||||
|
expect(find.byType(CharacterSelectionView), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('route returns a valid navigation route', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
Scaffold(
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push<void>(CharacterSelectionPage.route());
|
||||||
|
},
|
||||||
|
child: Text('Tap me'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tap me'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(CharacterSelectionPage), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CharacterSelectionView', () {
|
||||||
|
testWidgets('renders correctly', (tester) async {
|
||||||
|
const titleText = 'Choose your character!';
|
||||||
|
await tester.pumpApp(
|
||||||
|
CharacterSelectionView(),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(titleText), findsOneWidget);
|
||||||
|
expect(find.byType(CharacterImageButton), findsNWidgets(4));
|
||||||
|
expect(find.byType(TextButton), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('calls characterSelected when a character image is tapped',
|
||||||
|
(tester) async {
|
||||||
|
const sparkyButtonKey = Key('characterSelectionPage_sparkyButton');
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
CharacterSelectionView(),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(sparkyButtonKey));
|
||||||
|
|
||||||
|
verify(() => themeCubit.characterSelected(SparkyTheme())).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('navigates to PinballGamePage when start is tapped',
|
||||||
|
(tester) async {
|
||||||
|
final navigator = MockNavigator();
|
||||||
|
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
CharacterSelectionView(),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
navigator: navigator,
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(find.byType(TextButton));
|
||||||
|
await tester.tap(find.byType(TextButton));
|
||||||
|
|
||||||
|
verify(() => navigator.push<void>(any())).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('CharacterImageButton renders correctly', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
CharacterImageButton(DashTheme()),
|
||||||
|
themeCubit: themeCubit,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(Image), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue