diff --git a/lib/footer/footer.dart b/lib/footer/footer.dart deleted file mode 100644 index df3dbd2f..00000000 --- a/lib/footer/footer.dart +++ /dev/null @@ -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( - 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, - ), - ), - ], - ), - ); - } -} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 076ed336..2786b867 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -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( + builder: (context, state) { + return Visibility( + visible: state.status.isGameOver, + child: IconButton( + iconSize: 50, + icon: Assets.images.linkBox.infoIcon.image(), + onPressed: () => showMoreInformationDialog(context), + ), + ); + }, + ), + ); + } +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3d15d9fd..aa1a24f6 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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." diff --git a/lib/more_information/more_information.dart b/lib/more_information/more_information.dart new file mode 100644 index 00000000..317461ed --- /dev/null +++ b/lib/more_information/more_information.dart @@ -0,0 +1 @@ +export 'more_information_dialog.dart'; diff --git a/lib/more_information/more_information_dialog.dart b/lib/more_information/more_information_dialog.dart new file mode 100644 index 00000000..179c06f5 --- /dev/null +++ b/lib/more_information/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 showMoreInformationDialog(BuildContext context) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + + return showDialog( + 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( + 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'; +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index d1ecd72d..c4515ca8 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -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, + ); + }); + }); }); } diff --git a/test/footer/footer_test.dart b/test/more_information/more_information_dialog_test.dart similarity index 60% rename from test/footer/footer_test.dart rename to test/more_information/more_information_dialog_test.dart index 8f683cbf..f87ec84c 100644 --- a/test/footer/footer_test.dart +++ b/test/more_information/more_information_dialog_test.dart @@ -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() { ); }, ); + + { + 'Open Source Code': 'https://github.com/VGVentures/pinball', + 'Google I/O': 'https://events.google.com/io/', + 'Flutter Games': 'http://flutter.dev/games', + 'How it’s 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'), + ), + ); + }, + ); + }); }); }