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 'widgets/widgets.dart';
|
||||
|
@ -1 +1,2 @@
|
||||
export 'game_hud.dart';
|
||||
export 'game_over_dialog.dart';
|
||||
|
@ -1,4 +1,15 @@
|
||||
{
|
||||
"@@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",
|
||||
"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 '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;
|
||||
|
||||
export 'src/generated/generated.dart';
|
||||
export 'src/pinball_theme.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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
FlameTester<PinballGame> flameBlocTester({
|
||||
required GameBloc Function() gameBlocBuilder,
|
||||
required GameBloc gameBloc,
|
||||
}) {
|
||||
return FlameTester<PinballGame>(
|
||||
PinballGame.new,
|
||||
PinballGameX.initial,
|
||||
pumpWidget: (gameWidget, tester) async {
|
||||
await tester.pumpWidget(
|
||||
BlocProvider.value(
|
||||
value: gameBlocBuilder(),
|
||||
value: gameBloc,
|
||||
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