From 21b54a0cdcd4a3cc23853393e092704019827c06 Mon Sep 17 00:00:00 2001 From: jonathandaniels-vgv <102978796+jonathandaniels-vgv@users.noreply.github.com> Date: Fri, 29 Apr 2022 13:37:08 -0700 Subject: [PATCH] feat: added how to play dialog for desktop and mobile (#242) * chore: create app_ui package * chore: add dialog asset * chore: create DialogDecoration * chore: import app_ui * feat: use DialogDecoration in app * fix: update description * fix: rename package * fix: update README * fix: exlude generated files from analysis * fix: update workflow file * fix: update tests * fix: update description * added how to play content * added how to play screen for mobile devices (#245) * ui: added how to play dialog for mobile * chore: added mobile dialog content test * fix: apply code review * chore: addressed refactor feedback * feat: how to play dialog is timed and manually dismissible (#251) * chore: autoclose how to play dialog after 3 seconds * test: added test for barrier dismiss on tap * fix: update description * fix: add Material to decoration * chore: swapped asset for the spacebar * chore: removed duplicate variable * chore: moved timer logic to how to play dialog * ui: added rocket to mobile launch directions * feat(platform_helper): created a platform helper package to detect web vs mobile * feat(platform_helper): import platform helper into pinball * test: wrote tests for platform detection on how to play dialog * chore: added platform helper package to ci * chore: address foreseen nits Co-authored-by: arturplaczek Co-authored-by: arturplaczek <33895544+arturplaczek@users.noreply.github.com> --- .github/workflows/platform_helper.yaml | 23 ++ assets/images/components/key.png | Bin 0 -> 1267 bytes assets/images/components/space.png | Bin 0 -> 1606 bytes .../view/widgets/round_count_display.dart | 4 +- lib/game/view/widgets/score_view.dart | 2 +- lib/gen/assets.gen.dart | 8 + lib/l10n/arb/app_en.arb | 30 +- .../view/character_selection_page.dart | 3 +- .../widgets/how_to_play_dialog.dart | 330 ++++++++++++++---- lib/theme/app_colors.dart | 4 +- lib/theme/app_text_style.dart | 29 ++ packages/platform_helper/.gitignore | 39 +++ packages/platform_helper/README.md | 11 + .../platform_helper/analysis_options.yaml | 1 + .../platform_helper/lib/platform_helper.dart | 3 + .../lib/src/platform_helper.dart | 12 + packages/platform_helper/pubspec.yaml | 16 + .../test/src/platform_helper_test.dart | 39 +++ pubspec.lock | 7 + pubspec.yaml | 2 + .../widgets/round_count_display_test.dart | 4 +- .../view/character_selection_page_test.dart | 69 +++- .../widgets/how_to_play_dialog_test.dart | 59 +++- 23 files changed, 592 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/platform_helper.yaml create mode 100644 assets/images/components/key.png create mode 100644 assets/images/components/space.png create mode 100644 packages/platform_helper/.gitignore create mode 100644 packages/platform_helper/README.md create mode 100644 packages/platform_helper/analysis_options.yaml create mode 100644 packages/platform_helper/lib/platform_helper.dart create mode 100644 packages/platform_helper/lib/src/platform_helper.dart create mode 100644 packages/platform_helper/pubspec.yaml create mode 100644 packages/platform_helper/test/src/platform_helper_test.dart diff --git a/.github/workflows/platform_helper.yaml b/.github/workflows/platform_helper.yaml new file mode 100644 index 00000000..0c1c61e7 --- /dev/null +++ b/.github/workflows/platform_helper.yaml @@ -0,0 +1,23 @@ +name: platform_helper + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + + pull_request: + paths: + - "packages/platform_helper/**" + - ".github/workflows/platform_helper.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/platform_helper + coverage_excludes: "lib/gen/*.dart" diff --git a/assets/images/components/key.png b/assets/images/components/key.png new file mode 100644 index 0000000000000000000000000000000000000000..588c2b89f9e085b58a9ffe6de074bea356780b03 GIT binary patch literal 1267 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBZilCfV@L(#+q?VoqeDdwe0(gw@$r!|4qo3V z7C~#L4o0??7Xecz>a965H6bO*v16+;bI%9y102DsM=q>-b-_zk+cor7Nbcn$WkJ^e zKK|kl`R=*ba_94XpZ~5Y&$wFmHRt~KdpnhY_98&TA~8FW_KpmcZa3y#55i({@}3Sz#s9R zv)V`57k+)p{mDTq=Xt-)_k(BRH%_~)`ntB!@!R2B`$S{j=hbR$I9?NxJ-fX5^=fg> z(@MX?*Q}lU;MXfQ>*r!O|A$6=l3jgMO{e=!yXj3nJ^$mSjkeDYOTDiZiCKSns^`h? z9(F&MKG>{t=u5!5)@$!$*ZSYz3VzVYK8pMgZDG)N@m)!B^`#yQZwax3vGtVJ)#9 z&QCz@N5;g;C34pq3?21WwwtDGnCbm0QFgz81yC$W4W$0sssoB&H|(0t)PCjgqg2lM zU)D#+Xg-=$72z#=2xP!bPV4t_FW)EIs_XPGI{fR$@khHaSm}HJ5Rtw1@bHJK?uGJt zuI;9pJ5z%VmYrDoV#5oz^}@1Yzhf%){+xDT;foEw-nX*=Ka_y(WmlJ z(`wB@zlsYLbHVnNh7{>)XjcG6_#uT3EP z&)xs~vnZps=&O3!CYi4KrY}JiGdCC21l5~VR`y0)$*pV;eH^~f``71kXozEO^;`%GOB=*p!7d*?@PK$*C6T;%A`#&?EpBMiezQ)McPuy7Z_l7Vt7PeG* zvCrFcPT8%A`?h|1=;yGEZ;4y0ci#V`s580C^_hK+w$)Rh%46(1|DQLo=Rp(&FxG+B Zdzb>d_pg(bd{qV#^K|udS?83{1ORzRT%`a2 literal 0 HcmV?d00001 diff --git a/assets/images/components/space.png b/assets/images/components/space.png new file mode 100644 index 0000000000000000000000000000000000000000..9949b38368d1ef21614521fc7887aad81b773d02 GIT binary patch literal 1606 zcmX9;dsvfY6h9xt2FM`M1axsJkuqzFthov7!ys;oD=-Em7snly%bZ*mutWt@z(gu# z#GXunn$XZeh;svzO9m5*5h^24L7E>IaZ@+u`mjIV-+9k@&pFR|&Ut_57%ME0WWCxN z07$_>{+|LM8TegbNx-qcC8!)1R#`z&T!3XOmJR}Eg^sup;eHyp4VnhX)41|lir)@D z07>Pt#RLlgi@U-8ei3;HHY&>$2EVD8&wo76sHob~%FT6CAnURm)~-zzp(`yr-*EK{ z2(UVI$OLZw!G0F@_7?UQ+kWUc^j3tIS6PskL-s_@C$SX@_o86(-D7yR^}1ck$5_?K z8Ev8OP|!l}shVbI9|M0fZQDa%L&>=OuCC_DTnP03(;r9M(co`W9g{CDl%FAiN32#7 zd#z>U${2YAQBc*SXDi>|-!`LsOtE;S&8p*4N9*W9EAZH@m8kRXA5j@E=uIH%_&2;w zTcRiIf!lQH5F1E$@T3wW-V+6swUgX2FR-;Cq9#%TF)=)V8 zq&hd|8UHwiAZYaJj7)CnDVVN`#4QVBb`buq^U>fwXU#~vZmT9a*%2<>-px9i!(%V4{S)A z+N!CRg2K`Ey7ud4jI0R{ofIS8o60iFzt}g$X68T0vJ+(6_$nghc*6S_?x{D#kzd>y zkf^UZf#dZo{?pArZ0#v1_JEB?$90c}4_0WqLLlD8*gn|2V<4*dA#Jh&33+bVS)uF* zQ%BVm(}BhIRAsBlR5Y!RJ$sK9NMc2Msx;^1@#g5dKD=uy;GGRlnlA%YaKoKbsM`ag zUmggx!$p|`t3`(~zG-ps5>G#%e>e~1cORYVjy;V*{-FiwX?i}NzCVGaz{@{$f z;PQtlBvzx0Kjt0n!7)A`1r*_%;(T$%$ADDKng8=*TGlM@V^ionJ5iZzd7EIzNV3$PQwel;bFHTvW$vm zXTSV>!dgOj=YuA(d(-O~f{BSIXX)vJJ2zegjA3RCyYG!Yy}b;@Cc4W3eK;|BaS6w( zmT+PjBKZBr(9I=WaCxT9!Gn@N6C>#kE+(aripP}vwY(8lI<%SAK!Y&F6~bM=H>83n zXTB@N462W(#OC;~N3lI7S75b2pKBd+#AAp1qs${HGh!mFcbHnWp5+Xim`htOb zt>bm(n(pKtU5_G0b&V;nwW9HO-F^?gElMb9R}V6rH03pWNUYz+&!0m(Frj0Z<%P%Qrm&bxCk%UyyerzGkt|4+Iz0iST9SoygPto-JL_6XjIu2T}<+&-(u zC2ya{*j_0{Gt?`I&=S(EAH>2h8u}IF57GV8bPa7!j~vj*zQ260)%>a$9ze|GjGDWUg$C(WM}Ogcl`>2sOTdvT%urd+F6#B2u6 z^P9QJAMekasw2`ec{4s>h0`kg?N>CK-A8!EJoOnslW}v)D(DsS#mqNw!4*#_m-v`e z*TKh>A94Ryl-?eir7AB`sPju|&Sld_T zP@LF|-|;PyYP&}C^S?@(QSb6cnt9URQRC?OR$~U!I4IlbHdH*L>x@A}WyYE~lC;5B zRjIaufW2dK`}FaQHV7B#w`#cGnxV@}j5;GRA?K$`DSe;l^siY`Uh%2=v const AssetGenImage('assets/images/components/background.png'); + + /// File path: assets/images/components/key.png + AssetGenImage get key => + const AssetGenImage('assets/images/components/key.png'); + + /// File path: assets/images/components/space.png + AssetGenImage get space => + const AssetGenImage('assets/images/components/space.png'); } class $AssetsImagesScoreGen { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 9655d8be..19b12296 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -8,6 +8,10 @@ "@howToPlay": { "description": "Text displayed on the landing page how to play button" }, + "tipsForFlips": "Tips for flips", + "@tipsForFlips": { + "description": "Text displayed on the landing page how to play button" + }, "launchControls": "Launch Controls", "@launchControls": { "description": "Text displayed on the how to play dialog with the launch controls" @@ -16,6 +20,26 @@ "@flipperControls": { "description": "Text displayed on the how to play dialog with the flipper controls" }, + "tapAndHoldRocket": "Tap & Hold Rocket", + "@tapAndHoldRocket": { + "description": "Text displayed on the how to launch on mobile" + }, + "to": "to", + "@to": { + "description": "Text displayed for the word to" + }, + "launch": "LAUNCH", + "@launch": { + "description": "Text displayed for the word launch" + }, + "tapLeftRightScreen": "Tap left/right screen", + "@tapLeftRightScreen": { + "description": "Text displayed on the how to flip on mobile" + }, + "flip": "FLIP", + "@flip": { + "description": "Text displayed for the word FLIP" + }, "start": "Start", "@start": { "description": "Text displayed on the character selection page start button" @@ -24,6 +48,10 @@ "@select": { "description": "Text displayed on the character selection page select button" }, + "space": "Space", + "@space": { + "description": "Text displayed on space control button" + }, "characterSelectionTitle": "Choose your character!", "@characterSelectionTitle": { "description": "Title text displayed on the character selection page" @@ -80,4 +108,4 @@ "@rounds": { "description": "Text displayed on the scoreboard widget to indicate rounds left" } -} +} \ No newline at end of file diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 83dc6ee6..863722e6 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -49,14 +49,13 @@ class CharacterSelectionView extends StatelessWidget { Navigator.of(context).pop(); // TODO(arturplaczek): remove after merge StarBlocListener final height = MediaQuery.of(context).size.height * 0.5; - showDialog( context: context, builder: (_) => Center( child: SizedBox( height: height, width: height * 1.4, - child: const HowToPlayDialog(), + child: HowToPlayDialog(), ), ), ); diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index bc5166e4..1665d35d 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -1,33 +1,236 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; +import 'package:platform_helper/platform_helper.dart'; + +@visibleForTesting +enum Control { + left, + right, + down, + a, + d, + s, + space, +} + +extension on Control { + bool get isArrow => isDown || isRight || isLeft; + + bool get isDown => this == Control.down; + + bool get isRight => this == Control.right; + + bool get isLeft => this == Control.left; + + bool get isSpace => this == Control.space; + + String getCharacter(BuildContext context) { + switch (this) { + case Control.a: + return 'A'; + case Control.d: + return 'D'; + case Control.down: + return '>'; // Will be rotated + case Control.left: + return '<'; + case Control.right: + return '>'; + case Control.s: + return 'S'; + case Control.space: + return context.l10n.space; + } + } +} + +class HowToPlayDialog extends StatefulWidget { + HowToPlayDialog({ + Key? key, + @visibleForTesting PlatformHelper? platformHelper, + }) : platformHelper = platformHelper ?? PlatformHelper(), + super(key: key); + + final PlatformHelper platformHelper; + + @override + State createState() => _HowToPlayDialogState(); +} + +class _HowToPlayDialogState extends State { + late Timer closeTimer; + @override + void initState() { + super.initState(); + closeTimer = Timer(const Duration(seconds: 3), () { + if (mounted) { + Navigator.of(context).maybePop(); + } + }); + } + + @override + void dispose() { + closeTimer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = widget.platformHelper.isMobile; + return PixelatedDecoration( + header: const _HowToPlayHeader(), + body: isMobile ? const _MobileBody() : const _DesktopBody(), + ); + } +} + +class _MobileBody extends StatelessWidget { + const _MobileBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final paddingWidth = MediaQuery.of(context).size.width * 0.15; + final paddingHeight = MediaQuery.of(context).size.height * 0.075; + return FittedBox( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: paddingWidth, + ), + child: Column( + children: [ + const _MobileLaunchControls(), + SizedBox(height: paddingHeight), + const _MobileFlipperControls(), + ], + ), + ), + ); + } +} + +class _MobileLaunchControls extends StatelessWidget { + const _MobileLaunchControls({Key? key}) : super(key: key); -class HowToPlayDialog extends StatelessWidget { - const HowToPlayDialog({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapAndHoldRocket, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.launch, + style: textStyle.copyWith( + color: AppColors.blue, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _MobileFlipperControls extends StatelessWidget { + const _MobileFlipperControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final l10n = context.l10n; + const textStyle = AppTextStyle.subtitle3; + return Column( + children: [ + Text( + l10n.tapLeftRightScreen, + style: textStyle, + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${l10n.to} ', + style: textStyle, + ), + TextSpan( + text: l10n.flip, + style: textStyle.copyWith( + color: AppColors.orange, + ), + ), + ], + ), + ), + ], + ); + } +} + +class _DesktopBody extends StatelessWidget { + const _DesktopBody({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { const spacing = SizedBox(height: 16); + return ListView( + children: const [ + spacing, + _DesktopLaunchControls(), + spacing, + _DesktopFlipperControls(), + ], + ); + } +} - return PixelatedDecoration( - header: Text(l10n.howToPlay), - body: ListView( - children: const [ - spacing, - _LaunchControls(), - spacing, - _FlipperControls(), +class _HowToPlayHeader extends StatelessWidget { + const _HowToPlayHeader({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + const headerTextStyle = AppTextStyle.title; + + return FittedBox( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.howToPlay, + style: headerTextStyle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + l10n.tipsForFlips, + style: headerTextStyle, + ), ], ), ); } } -class _LaunchControls extends StatelessWidget { - const _LaunchControls({Key? key}) : super(key: key); +class _DesktopLaunchControls extends StatelessWidget { + const _DesktopLaunchControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -36,15 +239,18 @@ class _LaunchControls extends StatelessWidget { return Column( children: [ - Text(l10n.launchControls), + Text( + l10n.launchControls, + style: AppTextStyle.headline4, + ), const SizedBox(height: 10), Wrap( children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down), + KeyButton(control: Control.down), spacing, - KeyIndicator.fromKeyName(keyName: 'SPACE'), + KeyButton(control: Control.space), spacing, - KeyIndicator.fromKeyName(keyName: 'S'), + KeyButton(control: Control.s), ], ) ], @@ -52,8 +258,8 @@ class _LaunchControls extends StatelessWidget { } } -class _FlipperControls extends StatelessWidget { - const _FlipperControls({Key? key}) : super(key: key); +class _DesktopFlipperControls extends StatelessWidget { + const _DesktopFlipperControls({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -62,7 +268,10 @@ class _FlipperControls extends StatelessWidget { return Column( children: [ - Text(l10n.flipperControls), + Text( + l10n.flipperControls, + style: AppTextStyle.subtitle2, + ), const SizedBox(height: 10), Column( children: [ @@ -70,17 +279,17 @@ class _FlipperControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: const [ - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left), + KeyButton(control: Control.left), rowSpacing, - KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right), + KeyButton(control: Control.right), ], ), const SizedBox(height: 8), Wrap( children: const [ - KeyIndicator.fromKeyName(keyName: 'A'), + KeyButton(control: Control.a), rowSpacing, - KeyIndicator.fromKeyName(keyName: 'D'), + KeyButton(control: Control.d), ], ) ], @@ -90,65 +299,46 @@ class _FlipperControls extends StatelessWidget { } } -// TODO(allisonryan0002): remove visibility when adding final UI. @visibleForTesting -class KeyIndicator extends StatelessWidget { - const KeyIndicator._({ +class KeyButton extends StatelessWidget { + const KeyButton({ Key? key, - required String keyName, - required IconData keyIcon, - required bool fromIcon, - }) : _keyName = keyName, - _keyIcon = keyIcon, - _fromIcon = fromIcon, + required Control control, + }) : _control = control, super(key: key); - const KeyIndicator.fromKeyName({Key? key, required String keyName}) - : this._( - key: key, - keyName: keyName, - keyIcon: Icons.keyboard_arrow_down, - fromIcon: false, - ); - - const KeyIndicator.fromIcon({Key? key, required IconData keyIcon}) - : this._( - key: key, - keyName: '', - keyIcon: keyIcon, - fromIcon: true, - ); - - final String _keyName; - - final IconData _keyIcon; - - final bool _fromIcon; + final Control _control; @override Widget build(BuildContext context) { - const iconPadding = EdgeInsets.all(15); - const textPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 22); - final boarderColor = Colors.blue.withOpacity(0.5); - final color = Colors.blue.withOpacity(0.7); - + final textStyle = + _control.isArrow ? AppTextStyle.headline1 : AppTextStyle.headline3; + const height = 60.0; + final width = _control.isSpace ? height * 2.83 : height; return DecoratedBox( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all( - color: boarderColor, - width: 3, + image: DecorationImage( + fit: BoxFit.fill, + image: AssetImage( + _control.isSpace + ? Assets.images.components.space.keyName + : Assets.images.components.key.keyName, + ), ), ), - child: _fromIcon - ? Padding( - padding: iconPadding, - child: Icon(_keyIcon, color: color), - ) - : Padding( - padding: textPadding, - child: Text(_keyName, style: TextStyle(color: color)), + child: SizedBox( + width: width, + height: height, + child: Center( + child: RotatedBox( + quarterTurns: _control.isDown ? 1 : 0, + child: Text( + _control.getCharacter(context), + style: textStyle.copyWith(color: AppColors.white), ), + ), + ), + ), ); } } diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart index 2d3899a6..a12d3edc 100644 --- a/lib/theme/app_colors.dart +++ b/lib/theme/app_colors.dart @@ -7,7 +7,9 @@ abstract class AppColors { static const Color darkBlue = Color(0xFF0C32A4); - static const Color orange = Color(0xFFFFEE02); + static const Color yellow = Color(0xFFFFEE02); + + static const Color orange = Color(0xFFE5AB05); static const Color blue = Color(0xFF4B94F6); diff --git a/lib/theme/app_text_style.dart b/lib/theme/app_text_style.dart index 8104ca11..084936e9 100644 --- a/lib/theme/app_text_style.dart +++ b/lib/theme/app_text_style.dart @@ -27,6 +27,35 @@ abstract class AppTextStyle { fontFamily: _primaryFontFamily, ); + static const headline4 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const title = TextStyle( + color: AppColors.darkBlue, + fontSize: 20, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle3 = TextStyle( + color: AppColors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + + static const subtitle2 = TextStyle( + color: AppColors.white, + fontSize: 16, + package: _fontPackage, + fontFamily: _primaryFontFamily, + ); + static const subtitle1 = TextStyle( fontSize: 10, fontFamily: _primaryFontFamily, diff --git a/packages/platform_helper/.gitignore b/packages/platform_helper/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/platform_helper/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/platform_helper/README.md b/packages/platform_helper/README.md new file mode 100644 index 00000000..7a96e658 --- /dev/null +++ b/packages/platform_helper/README.md @@ -0,0 +1,11 @@ +# platform_helper + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Platform helper for Pinball application. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/platform_helper/analysis_options.yaml b/packages/platform_helper/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/platform_helper/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/platform_helper/lib/platform_helper.dart b/packages/platform_helper/lib/platform_helper.dart new file mode 100644 index 00000000..4b6d7f48 --- /dev/null +++ b/packages/platform_helper/lib/platform_helper.dart @@ -0,0 +1,3 @@ +library platform_helper; + +export 'src/platform_helper.dart'; diff --git a/packages/platform_helper/lib/src/platform_helper.dart b/packages/platform_helper/lib/src/platform_helper.dart new file mode 100644 index 00000000..638d1ab6 --- /dev/null +++ b/packages/platform_helper/lib/src/platform_helper.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +/// {@template platform_helper} +/// Returns whether the current platform is running on a mobile device. +/// {@endtemplate} +class PlatformHelper { + /// {@macro platform_helper} + bool get isMobile { + return defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.android; + } +} diff --git a/packages/platform_helper/pubspec.yaml b/packages/platform_helper/pubspec.yaml new file mode 100644 index 00000000..edff346a --- /dev/null +++ b/packages/platform_helper/pubspec.yaml @@ -0,0 +1,16 @@ +name: platform_helper +description: Platform helper for Pinball application. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/platform_helper/test/src/platform_helper_test.dart b/packages/platform_helper/test/src/platform_helper_test.dart new file mode 100644 index 00000000..69bec3a8 --- /dev/null +++ b/packages/platform_helper/test/src/platform_helper_test.dart @@ -0,0 +1,39 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_helper/platform_helper.dart'; + +void main() { + group('PlatformHelper', () { + test('can be instantiated', () { + expect(PlatformHelper(), isNotNull); + }); + + group('isMobile', () { + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is iOS', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test('returns true when defaultTargetPlatform is android', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + expect(PlatformHelper().isMobile, isTrue); + debugDefaultTargetPlatformOverride = null; + }); + + test( + 'returns false when defaultTargetPlatform is niether iOS nor android', + () async { + debugDefaultTargetPlatformOverride = TargetPlatform.macOS; + expect(PlatformHelper().isMobile, isFalse); + debugDefaultTargetPlatformOverride = null; + }, + ); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 4b71c77b..ab39378a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -513,6 +513,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + platform_helper: + dependency: "direct main" + description: + path: "packages/platform_helper" + relative: true + source: path + version: "1.0.0+1" plugin_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f129ea19..51c85cd5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: path: packages/pinball_theme pinball_ui: path: packages/pinball_ui + platform_helper: + path: packages/platform_helper dev_dependencies: bloc_test: ^9.0.2 diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index dfa28869..8f5f7f13 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -108,7 +108,7 @@ void main() { expect( find.byWidgetPredicate( - (widget) => widget is Container && widget.color == AppColors.orange, + (widget) => widget is Container && widget.color == AppColors.yellow, ), findsOneWidget, ); @@ -125,7 +125,7 @@ void main() { find.byWidgetPredicate( (widget) => widget is Container && - widget.color == AppColors.orange.withAlpha(128), + widget.color == AppColors.yellow.withAlpha(128), ), findsOneWidget, ); diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 0dda92d7..dc5d70ea 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_const_constructors +import 'dart:async'; + import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -84,17 +86,68 @@ void main() { .called(1); }); - testWidgets('displays how to play dialog when start is tapped', + group('HowToPlayDialog', () { + testWidgets( + 'is displayed for 3 seconds when start is tapped', (tester) async { - await tester.pumpApp( - CharacterSelectionView(), - characterThemeCubit: characterThemeCubit, + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(Duration(seconds: 3)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, ); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); + testWidgets( + 'can be dismissed manually before 3 seconds have passed', + (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionDialog.route()); + }, + child: Text('Tap me'), + ); + }, + ), + ), + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.tapAt(Offset(1, 1)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }, + ); }); }); diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index c31ac1a3..1de4c2ad 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -2,41 +2,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; +import 'package:platform_helper/platform_helper.dart'; import '../../helpers/helpers.dart'; +class MockPlatformHelper extends Mock implements PlatformHelper {} + void main() { group('HowToPlayDialog', () { - testWidgets('displays content', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - - await tester.pumpApp(HowToPlayDialog()); + late AppLocalizations l10n; + late PlatformHelper platformHelper; - expect(find.text(l10n.launchControls), findsOneWidget); + setUp(() async { + l10n = await AppLocalizations.delegate.load(Locale('en')); + platformHelper = MockPlatformHelper(); }); - }); - group('KeyIndicator', () { - testWidgets('fromKeyName renders correctly', (tester) async { - const keyName = 'A'; + testWidgets( + 'can be instantiated without passing in a platform helper', + (tester) async { + await tester.pumpApp(HowToPlayDialog()); + expect(find.byType(HowToPlayDialog), findsOneWidget); + }, + ); + testWidgets('displays content for desktop', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => false); await tester.pumpApp( - KeyIndicator.fromKeyName(keyName: keyName), + HowToPlayDialog( + platformHelper: platformHelper, + ), ); - - expect(find.text(keyName), findsOneWidget); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.launchControls), findsOneWidget); + expect(find.text(l10n.flipperControls), findsOneWidget); + expect(find.byType(KeyButton), findsNWidgets(7)); }); - testWidgets('fromIcon renders correctly', (tester) async { - const keyIcon = Icons.keyboard_arrow_down; + testWidgets('displays content for mobile', (tester) async { + when(() => platformHelper.isMobile).thenAnswer((_) => true); + await tester.pumpApp( + HowToPlayDialog( + platformHelper: platformHelper, + ), + ); + expect(find.text(l10n.howToPlay), findsOneWidget); + expect(find.text(l10n.tipsForFlips), findsOneWidget); + expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); + expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); + }); + }); + group('KeyButton', () { + testWidgets('renders correctly', (tester) async { await tester.pumpApp( - KeyIndicator.fromIcon(keyIcon: keyIcon), + KeyButton(control: Control.a), ); - expect(find.byIcon(keyIcon), findsOneWidget); + expect(find.text('A'), findsOneWidget); }); }); }