feat: added More Information (footer) dialog (#363)

pull/369/head
arturplaczek 2 years ago committed by GitHub
parent de76b54efc
commit 49fc1aa7f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,76 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// {@template footer}
/// Footer widget with links to the main tech stack.
/// {@endtemplate}
class Footer extends StatelessWidget {
/// {@macro footer}
const Footer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(50, 0, 50, 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
_MadeWithFlutterAndFirebase(),
_GoogleIO(),
],
),
);
}
}
class _GoogleIO extends StatelessWidget {
const _GoogleIO({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return Text(
l10n.footerGoogleIOText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
);
}
}
class _MadeWithFlutterAndFirebase extends StatelessWidget {
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: l10n.footerMadeWithText,
style: theme.textTheme.bodyText1!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.footerFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://flutter.dev'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.footerFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink('https://firebase.google.com'),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}

@ -7,7 +7,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -142,6 +144,7 @@ class PinballGameLoadedView extends StatelessWidget {
),
),
const _PositionedGameHud(),
const _PositionedInfoIcon(),
],
),
);
@ -159,6 +162,7 @@ class _PositionedGameHud extends StatelessWidget {
final isGameOver = context.select(
(GameBloc bloc) => bloc.state.status.isGameOver,
);
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
final screenWidth = MediaQuery.of(context).size.width;
final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8);
@ -174,3 +178,27 @@ class _PositionedGameHud extends StatelessWidget {
);
}
}
class _PositionedInfoIcon extends StatelessWidget {
const _PositionedInfoIcon({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
top: 0,
left: 0,
child: BlocBuilder<GameBloc, GameState>(
builder: (context, state) {
return Visibility(
visible: state.status.isGameOver,
child: IconButton(
iconSize: 50,
icon: Assets.images.linkBox.infoIcon.image(),
onPressed: () => showMoreInformationDialog(context),
),
);
},
),
);
}
}

@ -104,22 +104,6 @@
"@toSubmit": {
"description": "Ending text displayed on initials input screen informational text span"
},
"footerMadeWithText": "Made with ",
"@footerMadeWithText": {
"description": "Text shown on the footer which mentions technologies used to build the app."
},
"footerFlutterLinkText": "Flutter",
"@footerFlutterLinkText": {
"description": "Text on the link shown on the footer which navigates to the Flutter page"
},
"footerFirebaseLinkText": "Firebase",
"@footerFirebaseLinkText": {
"description": "Text on the link shown on the footer which navigates to the Firebase page"
},
"footerGoogleIOText": "Google I/O",
"@footerGoogleIOText": {
"description": "Text shown on the footer which mentions Google I/O"
},
"linkBoxTitle": "Resources",
"@linkBoxTitle": {
"description": "Text shown on the link box title section."

@ -0,0 +1 @@
export 'more_information_dialog.dart';

@ -0,0 +1,218 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball_ui/pinball_ui.dart';
/// Inflates [MoreInformationDialog] using [showDialog].
Future<void> showMoreInformationDialog(BuildContext context) {
final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16;
return showDialog<void>(
context: context,
barrierColor: PinballColors.transparent,
barrierDismissible: true,
builder: (_) {
return Center(
child: SizedBox(
height: gameWidgetWidth * 0.87,
width: gameWidgetWidth,
child: const MoreInformationDialog(),
),
);
},
);
}
/// {@template more_information_dialog}
/// Dialog used to show informational links
/// {@endtemplate}
class MoreInformationDialog extends StatelessWidget {
/// {@macro more_information_dialog}
const MoreInformationDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: PinballColors.transparent,
child: _LinkBoxDecoration(
child: Column(
children: const [
SizedBox(height: 16),
_LinkBoxHeader(),
Expanded(
child: _LinkBoxBody(),
),
],
),
),
);
}
}
class _LinkBoxHeader extends StatelessWidget {
const _LinkBoxHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final indent = MediaQuery.of(context).size.width / 5;
return Column(
children: [
Text(
l10n.linkBoxTitle,
style: Theme.of(context).textTheme.headline3!.copyWith(
color: PinballColors.blue,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Divider(
color: PinballColors.white,
endIndent: indent,
indent: indent,
thickness: 2,
),
],
);
}
}
class _LinkBoxDecoration extends StatelessWidget {
const _LinkBoxDecoration({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const CrtBackground().copyWith(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.all(
color: PinballColors.white,
width: 5,
),
),
child: child,
);
}
}
class _LinkBoxBody extends StatelessWidget {
const _LinkBoxBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const _MadeWithFlutterAndFirebase(),
_TextLink(
text: l10n.linkBoxOpenSourceCode,
link: _MoreInformationUrl.openSourceCode,
),
_TextLink(
text: l10n.linkBoxGoogleIOText,
link: _MoreInformationUrl.googleIOEvent,
),
_TextLink(
text: l10n.linkBoxFlutterGames,
link: _MoreInformationUrl.flutterGamesWebsite,
),
_TextLink(
text: l10n.linkBoxHowItsMade,
link: _MoreInformationUrl.howItsMadeArticle,
),
_TextLink(
text: l10n.linkBoxTermsOfService,
link: _MoreInformationUrl.termsOfService,
),
_TextLink(
text: l10n.linkBoxPrivacyPolicy,
link: _MoreInformationUrl.privacyPolicy,
),
],
);
}
}
class _TextLink extends StatelessWidget {
const _TextLink({
Key? key,
required this.text,
required this.link,
}) : super(key: key);
final String text;
final String link;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: () => openLink(link),
child: Text(
text,
style: theme.textTheme.headline5!.copyWith(
color: PinballColors.white,
),
overflow: TextOverflow.ellipsis,
),
);
}
}
class _MadeWithFlutterAndFirebase extends StatelessWidget {
const _MadeWithFlutterAndFirebase({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: l10n.linkBoxMadeWithText,
style: theme.textTheme.headline5!.copyWith(color: PinballColors.white),
children: <TextSpan>[
TextSpan(
text: l10n.linkBoxFlutterLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.flutterWebsite),
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' & '),
TextSpan(
text: l10n.linkBoxFirebaseLinkText,
recognizer: TapGestureRecognizer()
..onTap = () => openLink(_MoreInformationUrl.firebaseWebsite),
style: theme.textTheme.headline5!.copyWith(
decoration: TextDecoration.underline,
),
),
],
),
);
}
}
abstract class _MoreInformationUrl {
static const flutterWebsite = 'https://flutter.dev';
static const firebaseWebsite = 'https://firebase.google.com';
static const openSourceCode = 'https://github.com/VGVentures/pinball';
static const googleIOEvent = 'https://events.google.com/io/';
static const flutterGamesWebsite = 'http://flutter.dev/games';
static const howItsMadeArticle =
'https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d';
static const termsOfService = 'https://policies.google.com/terms';
static const privacyPolicy = 'https://policies.google.com/privacy';
}

@ -10,7 +10,9 @@ import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/assets_manager/assets_manager.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/gen/gen.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball/select_character/select_character.dart';
import 'package:pinball/start_game/start_game.dart';
import 'package:pinball_audio/pinball_audio.dart';
@ -331,5 +333,52 @@ void main() {
expect(game.focusNode.hasFocus, isTrue);
});
group('info icon', () {
testWidgets('renders on game over', (tester) async {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
expect(
find.image(Assets.images.linkBox.infoIcon),
findsOneWidget,
);
});
testWidgets('opens MoreInformationDialog when tapped', (tester) async {
final gameState = GameState.initial().copyWith(
status: GameStatus.gameOver,
);
whenListen(
gameBloc,
Stream.value(gameState),
initialState: gameState,
);
await tester.pumpApp(
PinballGameView(game: game),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
await tester.tap(find.byType(IconButton));
await tester.pump();
expect(
find.byType(MoreInformationDialog),
findsOneWidget,
);
});
});
});
}

@ -2,7 +2,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/footer/footer.dart';
import 'package:pinball/more_information/more_information.dart';
import 'package:pinball_ui/pinball_ui.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
@ -28,15 +28,34 @@ class _MockUrlLauncher extends Mock
implements UrlLauncherPlatform {}
void main() {
group('Footer', () {
group('MoreInformationDialog', () {
late UrlLauncherPlatform urlLauncher;
setUp(() async {
urlLauncher = _MockUrlLauncher();
UrlLauncherPlatform.instance = urlLauncher;
});
group('showMoreInformationDialog', () {
testWidgets('inflates the dialog', (tester) async {
await tester.pumpApp(
Builder(
builder: (context) {
return TextButton(
onPressed: () => showMoreInformationDialog(context),
child: const Text('test'),
);
},
),
);
await tester.tap(find.text('test'));
await tester.pump();
expect(find.byType(MoreInformationDialog), findsOneWidget);
});
});
testWidgets('renders "Made with..." and "Google I/O"', (tester) async {
await tester.pumpApp(const Footer());
await tester.pumpApp(const MoreInformationDialog());
expect(find.text('Google I/O'), findsOneWidget);
expect(
find.byWidgetPredicate(
@ -63,7 +82,7 @@ void main() {
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
await tester.pumpApp(const MoreInformationDialog());
final flutterTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && _tapTextSpan(widget, 'Flutter'),
);
@ -98,7 +117,7 @@ void main() {
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const Footer());
await tester.pumpApp(const MoreInformationDialog());
final firebaseTextFinder = find.byWidgetPredicate(
(widget) => widget is RichText && _tapTextSpan(widget, 'Firebase'),
);
@ -117,5 +136,50 @@ void main() {
);
},
);
<String, String>{
'Open Source Code': 'https://github.com/VGVentures/pinball',
'Google I/O': 'https://events.google.com/io/',
'Flutter Games': 'http://flutter.dev/games',
'How its made':
'https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d',
'Terms of Service': 'https://policies.google.com/terms',
'Privacy Policy': 'https://policies.google.com/privacy',
}.forEach((text, link) {
testWidgets(
'tapping on "$text" opens the link - $link',
(tester) async {
when(() => urlLauncher.canLaunch(any()))
.thenAnswer((_) async => true);
when(
() => urlLauncher.launch(
link,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
).thenAnswer((_) async => true);
await tester.pumpApp(const MoreInformationDialog());
await tester.tap(find.text(text));
await tester.pumpAndSettle();
verify(
() => urlLauncher.launch(
link,
useSafariVC: any(named: 'useSafariVC'),
useWebView: any(named: 'useWebView'),
enableJavaScript: any(named: 'enableJavaScript'),
enableDomStorage: any(named: 'enableDomStorage'),
universalLinksOnly: any(named: 'universalLinksOnly'),
headers: any(named: 'headers'),
),
);
},
);
});
});
}
Loading…
Cancel
Save