@ -0,0 +1,22 @@
|
|||||||
|
name: authentication_repository
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "packages/authentication_repository/**"
|
||||||
|
- ".github/workflows/authentication_repository.yaml"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "packages/authentication_repository/**"
|
||||||
|
- ".github/workflows/authentication_repository.yaml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
|
||||||
|
with:
|
||||||
|
working_directory: packages/authentication_repository
|
@ -0,0 +1,76 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"@@locale": "es",
|
|
||||||
"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,2 +0,0 @@
|
|||||||
export 'app_colors.dart';
|
|
||||||
export 'app_text_style.dart';
|
|
@ -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
|
@ -0,0 +1,11 @@
|
|||||||
|
# authentication_repository
|
||||||
|
|
||||||
|
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
|
||||||
|
[![License: MIT][license_badge]][license_link]
|
||||||
|
|
||||||
|
Repository to manage user authentication.
|
||||||
|
|
||||||
|
[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
|
@ -0,0 +1 @@
|
|||||||
|
include: package:very_good_analysis/analysis_options.2.4.0.yaml
|
@ -0,0 +1,3 @@
|
|||||||
|
library authentication_repository;
|
||||||
|
|
||||||
|
export 'src/authentication_repository.dart';
|
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
/// {@template authentication_exception}
|
||||||
|
/// Exception for authentication repository failures.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AuthenticationException implements Exception {
|
||||||
|
/// {@macro authentication_exception}
|
||||||
|
const AuthenticationException(this.error, this.stackTrace);
|
||||||
|
|
||||||
|
/// The error that was caught.
|
||||||
|
final Object error;
|
||||||
|
|
||||||
|
/// The Stacktrace associated with the [error].
|
||||||
|
final StackTrace stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template authentication_repository}
|
||||||
|
/// Repository to manage user authentication.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class AuthenticationRepository {
|
||||||
|
/// {@macro authentication_repository}
|
||||||
|
AuthenticationRepository(this._firebaseAuth);
|
||||||
|
|
||||||
|
final FirebaseAuth _firebaseAuth;
|
||||||
|
|
||||||
|
/// Sign in the existing user anonymously using [FirebaseAuth]. If the
|
||||||
|
/// authentication process can't be completed, it will throw an
|
||||||
|
/// [AuthenticationException].
|
||||||
|
Future<void> authenticateAnonymously() async {
|
||||||
|
try {
|
||||||
|
await _firebaseAuth.signInAnonymously();
|
||||||
|
} on Exception catch (error, stackTrace) {
|
||||||
|
throw AuthenticationException(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
name: authentication_repository
|
||||||
|
description: Repository to manage user authentication.
|
||||||
|
version: 1.0.0+1
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.16.0 <3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
firebase_auth: ^3.3.16
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
mocktail: ^0.2.0
|
||||||
|
very_good_analysis: ^2.4.0
|
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:authentication_repository/authentication_repository.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockFirebaseAuth extends Mock implements FirebaseAuth {}
|
||||||
|
|
||||||
|
class MockUserCredential extends Mock implements UserCredential {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late FirebaseAuth firebaseAuth;
|
||||||
|
late UserCredential userCredential;
|
||||||
|
late AuthenticationRepository authenticationRepository;
|
||||||
|
|
||||||
|
group('AuthenticationRepository', () {
|
||||||
|
setUp(() {
|
||||||
|
firebaseAuth = MockFirebaseAuth();
|
||||||
|
userCredential = MockUserCredential();
|
||||||
|
authenticationRepository = AuthenticationRepository(firebaseAuth);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('authenticateAnonymously', () {
|
||||||
|
test('completes if no exception is thrown', () async {
|
||||||
|
when(() => firebaseAuth.signInAnonymously())
|
||||||
|
.thenAnswer((_) async => userCredential);
|
||||||
|
await authenticationRepository.authenticateAnonymously();
|
||||||
|
verify(() => firebaseAuth.signInAnonymously()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws AuthenticationException when firebase auth fails', () async {
|
||||||
|
when(() => firebaseAuth.signInAnonymously())
|
||||||
|
.thenThrow(Exception('oops'));
|
||||||
|
expect(
|
||||||
|
() => authenticationRepository.authenticateAnonymously(),
|
||||||
|
throwsA(isA<AuthenticationException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 616 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 231 KiB |
Before Width: | Height: | Size: 38 KiB |
@ -0,0 +1,209 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
|
import 'package:pinball_components/gen/assets.gen.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
class AndroidSpaceship extends Blueprint {
|
||||||
|
AndroidSpaceship({required Vector2 position})
|
||||||
|
: super(
|
||||||
|
components: [
|
||||||
|
_SpaceshipSaucer()..initialPosition = position,
|
||||||
|
_SpaceshipSaucerSpriteAnimationComponent()..position = position,
|
||||||
|
_LightBeamSpriteComponent()..position = position + Vector2(2.5, 5),
|
||||||
|
_AndroidHead()..initialPosition = position + Vector2(0.5, 0.25),
|
||||||
|
_SpaceshipHole(
|
||||||
|
outsideLayer: Layer.spaceshipExitRail,
|
||||||
|
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
||||||
|
)..initialPosition = position - Vector2(5.3, -5.4),
|
||||||
|
_SpaceshipHole(
|
||||||
|
outsideLayer: Layer.board,
|
||||||
|
outsidePriority: RenderPriority.ballOnBoard,
|
||||||
|
)..initialPosition = position - Vector2(-7.5, -1.1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
||||||
|
_SpaceshipSaucer() : super(renderBody: false) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = _SpaceshipSaucerShape();
|
||||||
|
final bodyDef = BodyDef(
|
||||||
|
position: initialPosition,
|
||||||
|
userData: this,
|
||||||
|
angle: -1.7,
|
||||||
|
);
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixtureFromShape(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucerShape extends ChainShape {
|
||||||
|
_SpaceshipSaucerShape() {
|
||||||
|
const minorRadius = 9.75;
|
||||||
|
const majorRadius = 11.9;
|
||||||
|
|
||||||
|
createChain(
|
||||||
|
[
|
||||||
|
for (var angle = 0.2618; angle <= 6.0214; angle += math.pi / 180)
|
||||||
|
Vector2(
|
||||||
|
minorRadius * math.cos(angle),
|
||||||
|
majorRadius * math.sin(angle),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipSaucerSpriteAnimationComponent extends SpriteAnimationComponent
|
||||||
|
with HasGameRef {
|
||||||
|
_SpaceshipSaucerSpriteAnimationComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
priority: RenderPriority.spaceshipSaucer,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final spriteSheet = gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.saucer.keyName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountPerRow = 5;
|
||||||
|
const amountPerColumn = 3;
|
||||||
|
final textureSize = Vector2(
|
||||||
|
spriteSheet.width / amountPerRow,
|
||||||
|
spriteSheet.height / amountPerColumn,
|
||||||
|
);
|
||||||
|
size = textureSize / 10;
|
||||||
|
|
||||||
|
animation = SpriteAnimation.fromFrameData(
|
||||||
|
spriteSheet,
|
||||||
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: amountPerRow * amountPerColumn,
|
||||||
|
amountPerRow: amountPerRow,
|
||||||
|
stepTime: 1 / 24,
|
||||||
|
textureSize: textureSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(allisonryan0002): add pulsing behavior.
|
||||||
|
class _LightBeamSpriteComponent extends SpriteComponent with HasGameRef {
|
||||||
|
_LightBeamSpriteComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
priority: RenderPriority.spaceshipLightBeam,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.lightBeam.keyName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AndroidHead extends BodyComponent with InitialPosition, Layered {
|
||||||
|
_AndroidHead()
|
||||||
|
: super(
|
||||||
|
priority: RenderPriority.androidHead,
|
||||||
|
children: [_AndroidHeadSpriteAnimationComponent()],
|
||||||
|
renderBody: false,
|
||||||
|
) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Body createBody() {
|
||||||
|
final shape = EllipseShape(
|
||||||
|
center: Vector2.zero(),
|
||||||
|
majorRadius: 3.1,
|
||||||
|
minorRadius: 2,
|
||||||
|
)..rotate(1.4);
|
||||||
|
// TODO(allisonryan0002): use bumping behavior.
|
||||||
|
final fixtureDef = FixtureDef(
|
||||||
|
shape,
|
||||||
|
restitution: 0.1,
|
||||||
|
);
|
||||||
|
final bodyDef = BodyDef(position: initialPosition);
|
||||||
|
|
||||||
|
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AndroidHeadSpriteAnimationComponent extends SpriteAnimationComponent
|
||||||
|
with HasGameRef {
|
||||||
|
_AndroidHeadSpriteAnimationComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
position: Vector2(-0.24, -2.6),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final spriteSheet = gameRef.images.fromCache(
|
||||||
|
Assets.images.android.spaceship.animatronic.keyName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountPerRow = 18;
|
||||||
|
const amountPerColumn = 4;
|
||||||
|
final textureSize = Vector2(
|
||||||
|
spriteSheet.width / amountPerRow,
|
||||||
|
spriteSheet.height / amountPerColumn,
|
||||||
|
);
|
||||||
|
size = textureSize / 10;
|
||||||
|
|
||||||
|
animation = SpriteAnimation.fromFrameData(
|
||||||
|
spriteSheet,
|
||||||
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: amountPerRow * amountPerColumn,
|
||||||
|
amountPerRow: amountPerRow,
|
||||||
|
stepTime: 1 / 24,
|
||||||
|
textureSize: textureSize,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceshipHole extends LayerSensor {
|
||||||
|
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
||||||
|
: super(
|
||||||
|
insideLayer: Layer.spaceship,
|
||||||
|
outsideLayer: outsideLayer,
|
||||||
|
orientation: LayerEntranceOrientation.down,
|
||||||
|
insidePriority: RenderPriority.ballOnSpaceship,
|
||||||
|
outsidePriority: outsidePriority,
|
||||||
|
) {
|
||||||
|
layer = Layer.spaceship;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Shape get shape {
|
||||||
|
return ArcShape(
|
||||||
|
center: Vector2(0, -3.2),
|
||||||
|
arcRadius: 5,
|
||||||
|
angle: 1,
|
||||||
|
rotation: -2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
class BoardBackgroundSpriteComponent extends SpriteComponent with HasGameRef {
|
||||||
|
BoardBackgroundSpriteComponent()
|
||||||
|
: super(
|
||||||
|
anchor: Anchor.center,
|
||||||
|
priority: RenderPriority.boardBackground,
|
||||||
|
position: Vector2(0, -1),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final sprite = Sprite(
|
||||||
|
gameRef.images.fromCache(
|
||||||
|
Assets.images.boardBackground.keyName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.sprite = sprite;
|
||||||
|
size = sprite.originalSize / 10;
|
||||||
|
}
|
||||||
|
}
|
@ -1,246 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:pinball_components/gen/assets.gen.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
/// {@template spaceship}
|
|
||||||
/// A [Blueprint] which creates the spaceship feature.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class Spaceship extends Blueprint {
|
|
||||||
/// {@macro spaceship}
|
|
||||||
Spaceship({required Vector2 position})
|
|
||||||
: super(
|
|
||||||
components: [
|
|
||||||
SpaceshipSaucer()..initialPosition = position,
|
|
||||||
_SpaceshipEntrance()..initialPosition = position,
|
|
||||||
AndroidHead()..initialPosition = position,
|
|
||||||
_SpaceshipHole(
|
|
||||||
outsideLayer: Layer.spaceshipExitRail,
|
|
||||||
outsidePriority: RenderPriority.ballOnSpaceshipRail,
|
|
||||||
)..initialPosition = position - Vector2(5.2, -4.8),
|
|
||||||
_SpaceshipHole(
|
|
||||||
outsideLayer: Layer.board,
|
|
||||||
outsidePriority: RenderPriority.ballOnBoard,
|
|
||||||
)..initialPosition = position - Vector2(-7.2, -0.8),
|
|
||||||
SpaceshipWall()..initialPosition = position,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Total size of the spaceship.
|
|
||||||
static final size = Vector2(25, 19);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_saucer}
|
|
||||||
/// A [BodyComponent] for the base, or the saucer of the spaceship
|
|
||||||
/// {@endtemplate}
|
|
||||||
class SpaceshipSaucer extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_saucer}
|
|
||||||
SpaceshipSaucer()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.spaceshipSaucer,
|
|
||||||
renderBody: false,
|
|
||||||
children: [
|
|
||||||
_SpaceshipSaucerSpriteComponent(),
|
|
||||||
],
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = CircleShape()..radius = 3;
|
|
||||||
final fixtureDef = FixtureDef(
|
|
||||||
shape,
|
|
||||||
isSensor: true,
|
|
||||||
);
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipSaucerSpriteComponent extends SpriteComponent with HasGameRef {
|
|
||||||
_SpaceshipSaucerSpriteComponent()
|
|
||||||
: super(
|
|
||||||
anchor: Anchor.center,
|
|
||||||
// TODO(alestiago): Refactor to use sprite orignial size instead.
|
|
||||||
size: Spaceship.size,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
// TODO(alestiago): Use cached sprite.
|
|
||||||
sprite = await gameRef.loadSprite(
|
|
||||||
Assets.images.spaceship.saucer.keyName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_bridge}
|
|
||||||
/// A [BodyComponent] that provides both the collision and the rotation
|
|
||||||
/// animation for the bridge.
|
|
||||||
/// {@endtemplate}
|
|
||||||
class AndroidHead extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_bridge}
|
|
||||||
AndroidHead()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.androidHead,
|
|
||||||
children: [_AndroidHeadSpriteAnimation()],
|
|
||||||
renderBody: false,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final circleShape = CircleShape()..radius = 2;
|
|
||||||
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)
|
|
||||||
..createFixture(
|
|
||||||
FixtureDef(circleShape)..restitution = 0.4,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AndroidHeadSpriteAnimation extends SpriteAnimationComponent
|
|
||||||
with HasGameRef {
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
final image = await gameRef.images.load(
|
|
||||||
Assets.images.spaceship.bridge.keyName,
|
|
||||||
);
|
|
||||||
size = Vector2(8.2, 10);
|
|
||||||
position = Vector2(0, -2);
|
|
||||||
anchor = Anchor.center;
|
|
||||||
|
|
||||||
final data = SpriteAnimationData.sequenced(
|
|
||||||
amount: 72,
|
|
||||||
amountPerRow: 24,
|
|
||||||
stepTime: 0.05,
|
|
||||||
textureSize: size * 10,
|
|
||||||
);
|
|
||||||
animation = SpriteAnimation.fromFrameData(image, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipEntrance extends LayerSensor {
|
|
||||||
_SpaceshipEntrance()
|
|
||||||
: super(
|
|
||||||
insideLayer: Layer.spaceship,
|
|
||||||
orientation: LayerEntranceOrientation.up,
|
|
||||||
insidePriority: RenderPriority.ballOnSpaceship,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Shape get shape {
|
|
||||||
final radius = Spaceship.size.y / 2;
|
|
||||||
return PolygonShape()
|
|
||||||
..setAsEdge(
|
|
||||||
Vector2(
|
|
||||||
radius * cos(20 * pi / 180),
|
|
||||||
radius * sin(20 * pi / 180),
|
|
||||||
)..rotate(90 * pi / 180),
|
|
||||||
Vector2(
|
|
||||||
radius * cos(340 * pi / 180),
|
|
||||||
radius * sin(340 * pi / 180),
|
|
||||||
)..rotate(90 * pi / 180),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SpaceshipHole extends LayerSensor {
|
|
||||||
_SpaceshipHole({required Layer outsideLayer, required int outsidePriority})
|
|
||||||
: super(
|
|
||||||
insideLayer: Layer.spaceship,
|
|
||||||
outsideLayer: outsideLayer,
|
|
||||||
orientation: LayerEntranceOrientation.down,
|
|
||||||
insidePriority: RenderPriority.ballOnSpaceship,
|
|
||||||
outsidePriority: outsidePriority,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Shape get shape {
|
|
||||||
return ArcShape(
|
|
||||||
center: Vector2(0, -3.2),
|
|
||||||
arcRadius: 5,
|
|
||||||
angle: 1,
|
|
||||||
rotation: -2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_wall_shape}
|
|
||||||
/// The [ChainShape] that defines the shape of the [SpaceshipWall].
|
|
||||||
/// {@endtemplate}
|
|
||||||
class _SpaceshipWallShape extends ChainShape {
|
|
||||||
/// {@macro spaceship_wall_shape}
|
|
||||||
_SpaceshipWallShape() {
|
|
||||||
final minorRadius = (Spaceship.size.y - 2) / 2;
|
|
||||||
final majorRadius = (Spaceship.size.x - 2) / 2;
|
|
||||||
|
|
||||||
createChain(
|
|
||||||
[
|
|
||||||
// TODO(alestiago): Try converting this logic to radian.
|
|
||||||
for (var angle = 20; angle <= 340; angle++)
|
|
||||||
Vector2(
|
|
||||||
minorRadius * cos(angle * pi / 180),
|
|
||||||
majorRadius * sin(angle * pi / 180),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// {@template spaceship_wall}
|
|
||||||
/// A [BodyComponent] that provides the collision for the wall
|
|
||||||
/// surrounding the spaceship.
|
|
||||||
///
|
|
||||||
/// It has a small opening to allow the [Ball] to get inside the spaceship
|
|
||||||
/// saucer.
|
|
||||||
///
|
|
||||||
/// It also contains the [SpriteComponent] for the lower wall
|
|
||||||
/// {@endtemplate}
|
|
||||||
class SpaceshipWall extends BodyComponent with InitialPosition, Layered {
|
|
||||||
/// {@macro spaceship_wall}
|
|
||||||
SpaceshipWall()
|
|
||||||
: super(
|
|
||||||
priority: RenderPriority.spaceshipSaucerWall,
|
|
||||||
renderBody: false,
|
|
||||||
) {
|
|
||||||
layer = Layer.spaceship;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Body createBody() {
|
|
||||||
final shape = _SpaceshipWallShape();
|
|
||||||
final fixtureDef = FixtureDef(shape);
|
|
||||||
|
|
||||||
final bodyDef = BodyDef(
|
|
||||||
position: initialPosition,
|
|
||||||
userData: this,
|
|
||||||
angle: -1.7,
|
|
||||||
);
|
|
||||||
|
|
||||||
return world.createBody(bodyDef)..createFixture(fixtureDef);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
import 'package:sandbox/stories/ball/basic_ball_game.dart';
|
||||||
|
|
||||||
|
class AndroidSpaceshipGame extends BallGame {
|
||||||
|
AndroidSpaceshipGame()
|
||||||
|
: super(
|
||||||
|
ballPriority: RenderPriority.ballOnSpaceship,
|
||||||
|
ballLayer: Layer.spaceship,
|
||||||
|
imagesFileNames: [
|
||||||
|
Assets.images.android.spaceship.saucer.keyName,
|
||||||
|
Assets.images.android.spaceship.animatronic.keyName,
|
||||||
|
Assets.images.android.spaceship.lightBeam.keyName,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
static const description = '''
|
||||||
|
Shows how the AndroidSpaceship is rendered.
|
||||||
|
|
||||||
|
- Activate the "trace" parameter to overlay the body.
|
||||||
|
- Tap anywhere on the screen to spawn a Ball into the game.
|
||||||
|
''';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
camera.followVector2(Vector2.zero());
|
||||||
|
await addFromBlueprint(
|
||||||
|
AndroidSpaceship(position: Vector2.zero()),
|
||||||
|
);
|
||||||
|
|
||||||
|
await traceAllBodies();
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flame/input.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
import 'package:sandbox/common/common.dart';
|
|
||||||
|
|
||||||
class SpaceshipGame extends AssetsGame with TapDetector {
|
|
||||||
static const description = '''
|
|
||||||
Shows how a Spaceship works.
|
|
||||||
|
|
||||||
- Tap anywhere on the screen to spawn a Ball into the game.
|
|
||||||
''';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
camera.followVector2(Vector2.zero());
|
|
||||||
await addFromBlueprint(
|
|
||||||
Spaceship(position: Vector2.zero()),
|
|
||||||
);
|
|
||||||
await ready();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onTapUp(TapUpInfo info) {
|
|
||||||
add(
|
|
||||||
Ball(baseColor: Colors.blue)
|
|
||||||
..initialPosition = info.eventPosition.game
|
|
||||||
..layer = Layer.spaceshipEntranceRamp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AndroidSpaceship', () {
|
||||||
|
group('Spaceship', () {
|
||||||
|
final assets = [
|
||||||
|
Assets.images.android.spaceship.saucer.keyName,
|
||||||
|
Assets.images.android.spaceship.animatronic.keyName,
|
||||||
|
Assets.images.android.spaceship.lightBeam.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
flameTester.test('loads correctly', (game) async {
|
||||||
|
await game.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
|
||||||
|
await game.ready();
|
||||||
|
});
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'renders correctly',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
await game
|
||||||
|
.addFromBlueprint(AndroidSpaceship(position: Vector2.zero()));
|
||||||
|
game.camera.followVector2(Vector2.zero());
|
||||||
|
await game.ready();
|
||||||
|
await tester.pump();
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
final animationDuration = game
|
||||||
|
.descendants()
|
||||||
|
.whereType<SpriteAnimationComponent>()
|
||||||
|
.last
|
||||||
|
.animation!
|
||||||
|
.totalDuration();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/android_spaceship/start.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(animationDuration * 0.5);
|
||||||
|
await tester.pump();
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/android_spaceship/middle.png'),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.update(animationDuration * 0.5);
|
||||||
|
await tester.pump();
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/android_spaceship/end.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
// ignore_for_file: cascade_invocations
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final assets = [
|
||||||
|
Assets.images.boardBackground.keyName,
|
||||||
|
];
|
||||||
|
final flameTester = FlameTester(() => TestGame(assets));
|
||||||
|
|
||||||
|
group('BoardBackgroundSpriteComponent', () {
|
||||||
|
flameTester.test(
|
||||||
|
'loads correctly',
|
||||||
|
(game) async {
|
||||||
|
final boardBackground = BoardBackgroundSpriteComponent();
|
||||||
|
await game.ensureAdd(boardBackground);
|
||||||
|
|
||||||
|
expect(game.contains(boardBackground), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameTester.testGameWidget(
|
||||||
|
'renders correctly',
|
||||||
|
setUp: (game, tester) async {
|
||||||
|
await game.images.loadAll(assets);
|
||||||
|
final boardBackground = BoardBackgroundSpriteComponent();
|
||||||
|
await game.ensureAdd(boardBackground);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.camera
|
||||||
|
..followVector2(Vector2.zero())
|
||||||
|
..zoom = 3.7;
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
await expectLater(
|
||||||
|
find.byGame<TestGame>(),
|
||||||
|
matchesGoldenFile('golden/board-background.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 78 KiB |
@ -1,56 +0,0 @@
|
|||||||
// ignore_for_file: cascade_invocations
|
|
||||||
|
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
|
||||||
import 'package:flame_test/flame_test.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:mocktail/mocktail.dart';
|
|
||||||
import 'package:pinball_components/pinball_components.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
|
||||||
|
|
||||||
import '../../helpers/helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('Spaceship', () {
|
|
||||||
late Filter filterData;
|
|
||||||
late Fixture fixture;
|
|
||||||
late Body body;
|
|
||||||
late Ball ball;
|
|
||||||
late Forge2DGame game;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
filterData = MockFilter();
|
|
||||||
|
|
||||||
fixture = MockFixture();
|
|
||||||
when(() => fixture.filterData).thenReturn(filterData);
|
|
||||||
|
|
||||||
body = MockBody();
|
|
||||||
when(() => body.fixtures).thenReturn([fixture]);
|
|
||||||
|
|
||||||
game = MockGame();
|
|
||||||
|
|
||||||
ball = MockBall();
|
|
||||||
when(() => ball.gameRef).thenReturn(game);
|
|
||||||
when(() => ball.body).thenReturn(body);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Spaceship', () {
|
|
||||||
final tester = FlameTester(TestGame.new);
|
|
||||||
|
|
||||||
tester.testGameWidget(
|
|
||||||
'renders correctly',
|
|
||||||
setUp: (game, tester) async {
|
|
||||||
final position = Vector2(30, -30);
|
|
||||||
await game.addFromBlueprint(Spaceship(position: position));
|
|
||||||
game.camera.followVector2(position);
|
|
||||||
await game.ready();
|
|
||||||
},
|
|
||||||
verify: (game, tester) async {
|
|
||||||
await expectLater(
|
|
||||||
find.byGame<TestGame>(),
|
|
||||||
matchesGoldenFile('golden/spaceship.png'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,95 +1,105 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:pinball_flame/pinball_flame.dart';
|
import 'package:pinball_flame/pinball_flame.dart';
|
||||||
|
|
||||||
/// Appends a new [ContactCallbacks] to the parent.
|
/// Appends a new [ContactCallbacks] to the parent.
|
||||||
///
|
///
|
||||||
/// This is a convenience class for adding a [ContactCallbacks] to the parent.
|
/// This is a convenience class for adding a [ContactCallbacks] to the parent.
|
||||||
/// In constract with just adding a [ContactCallbacks] to the parent's body
|
/// In contrast with just assigning a [ContactCallbacks] to a userData, this
|
||||||
/// userData, this class respects the previous [ContactCallbacks] in the
|
/// class respects the previous userData.
|
||||||
/// parent's body userData, if any. Hence, it avoids overriding any previous
|
|
||||||
/// [ContactCallbacks] in the parent.
|
|
||||||
///
|
///
|
||||||
/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup],
|
/// It does so by grouping the userData in a [_UserData], and resetting the
|
||||||
/// and resetting the parent's userData accordingly.
|
/// parent's userData accordingly.
|
||||||
// TODO(alestiago): Make use of generics to infer the type of the contact.
|
// TODO(alestiago): Make use of generics to infer the type of the contact.
|
||||||
// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267
|
// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267
|
||||||
// TODO(alestiago): Consider if there is a need to support adjusting a fixture's
|
|
||||||
// userData.
|
|
||||||
class ContactBehavior<T extends BodyComponent> extends Component
|
class ContactBehavior<T extends BodyComponent> extends Component
|
||||||
with ContactCallbacks, ParentIsA<T> {
|
with ContactCallbacks, ParentIsA<T> {
|
||||||
|
final _fixturesUserData = <Object>{};
|
||||||
|
|
||||||
|
/// Specifies which fixtures should be considered for contact.
|
||||||
|
///
|
||||||
|
/// Fixtures are identifiable by their userData.
|
||||||
|
///
|
||||||
|
/// If no fixtures are specified, the [ContactCallbacks] is applied to the
|
||||||
|
/// entire body, hence all fixtures are considered.
|
||||||
|
void applyTo(Iterable<Object> userData) => _fixturesUserData.addAll(userData);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
final userData = parent.body.userData;
|
if (_fixturesUserData.isNotEmpty) {
|
||||||
if (userData is _ContactCallbacksGroup) {
|
for (final fixture in _targetedFixtures) {
|
||||||
userData.addContactCallbacks(this);
|
fixture.userData = _UserData.fromFixture(fixture)..add(this);
|
||||||
} else if (userData is ContactCallbacks) {
|
}
|
||||||
final contactCallbacksGroup = _ContactCallbacksGroup()
|
|
||||||
..addContactCallbacks(userData)
|
|
||||||
..addContactCallbacks(this);
|
|
||||||
parent.body.userData = contactCallbacksGroup;
|
|
||||||
} else {
|
} else {
|
||||||
parent.body.userData = this;
|
parent.body.userData = _UserData.fromBody(parent.body)..add(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Iterable<Fixture> get _targetedFixtures =>
|
||||||
|
parent.body.fixtures.where((fixture) {
|
||||||
|
if (_fixturesUserData.contains(fixture.userData)) return true;
|
||||||
|
|
||||||
|
final userData = fixture.userData;
|
||||||
|
if (userData is _UserData) {
|
||||||
|
return _fixturesUserData.contains(userData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactCallbacksGroup implements ContactCallbacks {
|
class _UserData with ContactCallbacks {
|
||||||
final List<ContactCallbacks> _contactCallbacks = [];
|
_UserData._(Object? userData) : _userData = [userData];
|
||||||
|
|
||||||
|
factory _UserData._fromUserData(Object? userData) {
|
||||||
|
if (userData is _UserData) return userData;
|
||||||
|
return _UserData._(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory _UserData.fromFixture(Fixture fixture) =>
|
||||||
|
_UserData._fromUserData(fixture.userData);
|
||||||
|
|
||||||
|
factory _UserData.fromBody(Body body) =>
|
||||||
|
_UserData._fromUserData(body.userData);
|
||||||
|
|
||||||
|
final List<Object?> _userData;
|
||||||
|
|
||||||
|
Iterable<ContactCallbacks> get _contactCallbacks =>
|
||||||
|
_userData.whereType<ContactCallbacks>();
|
||||||
|
|
||||||
|
Object? get value => _userData.first;
|
||||||
|
|
||||||
|
void add(Object? userData) => _userData.add(userData);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
|
||||||
void beginContact(Object other, Contact contact) {
|
void beginContact(Object other, Contact contact) {
|
||||||
onBeginContact?.call(other, contact);
|
super.beginContact(other, contact);
|
||||||
for (final callback in _contactCallbacks) {
|
for (final callback in _contactCallbacks) {
|
||||||
callback.beginContact(other, contact);
|
callback.beginContact(other, contact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
|
||||||
void endContact(Object other, Contact contact) {
|
void endContact(Object other, Contact contact) {
|
||||||
onEndContact?.call(other, contact);
|
super.endContact(other, contact);
|
||||||
for (final callback in _contactCallbacks) {
|
for (final callback in _contactCallbacks) {
|
||||||
callback.endContact(other, contact);
|
callback.endContact(other, contact);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
|
||||||
void preSolve(Object other, Contact contact, Manifold oldManifold) {
|
void preSolve(Object other, Contact contact, Manifold oldManifold) {
|
||||||
onPreSolve?.call(other, contact, oldManifold);
|
super.preSolve(other, contact, oldManifold);
|
||||||
for (final callback in _contactCallbacks) {
|
for (final callback in _contactCallbacks) {
|
||||||
callback.preSolve(other, contact, oldManifold);
|
callback.preSolve(other, contact, oldManifold);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
|
||||||
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
|
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
|
||||||
onPostSolve?.call(other, contact, impulse);
|
super.postSolve(other, contact, impulse);
|
||||||
for (final callback in _contactCallbacks) {
|
for (final callback in _contactCallbacks) {
|
||||||
callback.postSolve(other, contact, impulse);
|
callback.postSolve(other, contact, impulse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addContactCallbacks(ContactCallbacks callback) {
|
|
||||||
_contactCallbacks.add(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void Function(Object other, Contact contact)? onBeginContact;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void Function(Object other, Contact contact)? onEndContact;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void Function(Object other, Contact contact, ContactImpulse impulse)?
|
|
||||||
onPostSolve;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void Function(Object other, Contact contact, Manifold oldManifold)?
|
|
||||||
onPreSolve;
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
/// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
/// *****************************************************
|
||||||
|
/// FlutterGen
|
||||||
|
/// *****************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: directives_ordering,unnecessary_import
|
||||||
|
|
||||||
|
class FontFamily {
|
||||||
|
FontFamily._();
|
||||||
|
|
||||||
|
/// Font family: PixeloidMono
|
||||||
|
static const String pixeloidMono = 'PixeloidMono';
|
||||||
|
|
||||||
|
/// Font family: PixeloidSans
|
||||||
|
static const String pixeloidSans = 'PixeloidSans';
|
||||||
|
}
|
@ -1,3 +1,8 @@
|
|||||||
library pinball_ui;
|
library pinball_ui;
|
||||||
|
|
||||||
|
export 'package:url_launcher/url_launcher.dart';
|
||||||
|
export 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
|
||||||
|
|
||||||
export 'src/dialog/dialog.dart';
|
export 'src/dialog/dialog.dart';
|
||||||
|
export 'src/external_links/external_links.dart';
|
||||||
|
export 'src/theme/theme.dart';
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
/// Opens the given [url] in a new tab of the host browser
|
||||||
|
Future<void> openLink(String url, {VoidCallback? onError}) async {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
} else if (onError != null) {
|
||||||
|
onError();
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,11 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
abstract class AppColors {
|
abstract class PinballColors {
|
||||||
static const Color white = Color(0xFFFFFFFF);
|
static const Color white = Color(0xFFFFFFFF);
|
||||||
|
|
||||||
static const Color darkBlue = Color(0xFF0C32A4);
|
static const Color darkBlue = Color(0xFF0C32A4);
|
||||||
|
|
||||||
static const Color yellow = Color(0xFFFFEE02);
|
static const Color yellow = Color(0xFFFFEE02);
|
||||||
|
|
||||||
static const Color orange = Color(0xFFE5AB05);
|
static const Color orange = Color(0xFFE5AB05);
|
||||||
|
|
||||||
static const Color blue = Color(0xFF4B94F6);
|
static const Color blue = Color(0xFF4B94F6);
|
||||||
|
|
||||||
static const Color transparent = Color(0x00000000);
|
static const Color transparent = Color(0x00000000);
|
||||||
}
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
/// Pinball theme
|
||||||
|
class PinballTheme {
|
||||||
|
/// Standard [ThemeData] for Pinball UI
|
||||||
|
static ThemeData get standard {
|
||||||
|
return ThemeData(
|
||||||
|
textTheme: _textTheme,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextTheme get _textTheme {
|
||||||
|
return const TextTheme(
|
||||||
|
headline1: PinballTextStyle.headline1,
|
||||||
|
headline2: PinballTextStyle.headline2,
|
||||||
|
headline3: PinballTextStyle.headline3,
|
||||||
|
headline4: PinballTextStyle.headline4,
|
||||||
|
subtitle1: PinballTextStyle.subtitle1,
|
||||||
|
subtitle2: PinballTextStyle.subtitle2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export 'pinball_colors.dart';
|
||||||
|
export 'pinball_text_style.dart';
|
||||||
|
export 'pinball_theme.dart';
|
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
class MockUrlLauncher extends Mock
|
||||||
|
with MockPlatformInterfaceMixin
|
||||||
|
implements UrlLauncherPlatform {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late UrlLauncherPlatform urlLauncher;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
urlLauncher = MockUrlLauncher();
|
||||||
|
UrlLauncherPlatform.instance = urlLauncher;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('openLink', () {
|
||||||
|
test('launches the link', () async {
|
||||||
|
when(
|
||||||
|
() => urlLauncher.canLaunch(any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => true,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
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 openLink('uri');
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes the onError callback when it cannot launch', () async {
|
||||||
|
var wasCalled = false;
|
||||||
|
when(
|
||||||
|
() => urlLauncher.canLaunch(any()),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async => false,
|
||||||
|
);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
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 openLink(
|
||||||
|
'url',
|
||||||
|
onError: () {
|
||||||
|
wasCalled = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await expectLater(wasCalled, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PinballColors', () {
|
||||||
|
test('white is 0xFFFFFFFF', () {
|
||||||
|
expect(PinballColors.white, const Color(0xFFFFFFFF));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('darkBlue is 0xFF0C32A4', () {
|
||||||
|
expect(PinballColors.darkBlue, const Color(0xFF0C32A4));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('yellow is 0xFFFFEE02', () {
|
||||||
|
expect(PinballColors.yellow, const Color(0xFFFFEE02));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('orange is 0xFFE5AB05', () {
|
||||||
|
expect(PinballColors.orange, const Color(0xFFE5AB05));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blue is 0xFF4B94F6', () {
|
||||||
|
expect(PinballColors.blue, const Color(0xFF4B94F6));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transparent is 0x00000000', () {
|
||||||
|
expect(PinballColors.transparent, const Color(0x00000000));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PinballTextStyle', () {
|
||||||
|
test('headline1 has fontSize 28 and white color', () {
|
||||||
|
const style = PinballTextStyle.headline1;
|
||||||
|
expect(style.fontSize, 28);
|
||||||
|
expect(style.color, PinballColors.white);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline2 has fontSize 24', () {
|
||||||
|
const style = PinballTextStyle.headline2;
|
||||||
|
expect(style.fontSize, 24);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline3 has fontSize 20 and white color', () {
|
||||||
|
const style = PinballTextStyle.headline3;
|
||||||
|
expect(style.fontSize, 20);
|
||||||
|
expect(style.color, PinballColors.white);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline4 has fontSize 16 and white color', () {
|
||||||
|
const style = PinballTextStyle.headline4;
|
||||||
|
expect(style.fontSize, 16);
|
||||||
|
expect(style.color, PinballColors.white);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitle1 has fontSize 10 and yellow color', () {
|
||||||
|
const style = PinballTextStyle.subtitle1;
|
||||||
|
expect(style.fontSize, 10);
|
||||||
|
expect(style.color, PinballColors.yellow);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitle2 has fontSize 16 and white color', () {
|
||||||
|
const style = PinballTextStyle.subtitle2;
|
||||||
|
expect(style.fontSize, 16);
|
||||||
|
expect(style.color, PinballColors.white);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('PinballTheme', () {
|
||||||
|
group('standard', () {
|
||||||
|
test('headline1 matches PinballTextStyle#headline1', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline1!.fontSize,
|
||||||
|
PinballTextStyle.headline1.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline1!.color,
|
||||||
|
PinballTextStyle.headline1.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline1!.fontFamily,
|
||||||
|
PinballTextStyle.headline1.fontFamily,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline2 matches PinballTextStyle#headline2', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline2!.fontSize,
|
||||||
|
PinballTextStyle.headline2.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline2!.fontFamily,
|
||||||
|
PinballTextStyle.headline2.fontFamily,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline2!.fontWeight,
|
||||||
|
PinballTextStyle.headline2.fontWeight,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline3 matches PinballTextStyle#headline3', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline3!.fontSize,
|
||||||
|
PinballTextStyle.headline3.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline3!.color,
|
||||||
|
PinballTextStyle.headline3.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline3!.fontFamily,
|
||||||
|
PinballTextStyle.headline3.fontFamily,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headline4 matches PinballTextStyle#headline4', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline4!.fontSize,
|
||||||
|
PinballTextStyle.headline4.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline4!.color,
|
||||||
|
PinballTextStyle.headline4.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.headline4!.fontFamily,
|
||||||
|
PinballTextStyle.headline4.fontFamily,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitle1 matches PinballTextStyle#subtitle1', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle1!.fontSize,
|
||||||
|
PinballTextStyle.subtitle1.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle1!.color,
|
||||||
|
PinballTextStyle.subtitle1.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle1!.fontFamily,
|
||||||
|
PinballTextStyle.subtitle1.fontFamily,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitle2 matches PinballTextStyle#subtitle2', () {
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle2!.fontSize,
|
||||||
|
PinballTextStyle.subtitle2.fontSize,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle2!.color,
|
||||||
|
PinballTextStyle.subtitle2.color,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
PinballTheme.standard.textTheme.subtitle2!.fontFamily,
|
||||||
|
PinballTextStyle.subtitle2.fontFamily,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
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_ui/pinball_ui.dart';
|
||||||
|
|
||||||
|
import '../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Footer', () {
|
||||||
|
late UrlLauncherPlatform urlLauncher;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
urlLauncher = MockUrlLauncher();
|
||||||
|
UrlLauncherPlatform.instance = urlLauncher;
|
||||||
|
});
|
||||||
|
testWidgets('renders "Made with..." and "Google I/O"', (tester) async {
|
||||||
|
await tester.pumpApp(const Footer());
|
||||||
|
expect(find.text('Google I/O'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is RichText &&
|
||||||
|
widget.text.toPlainText() == 'Made with Flutter & Firebase',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'tapping on "Flutter" opens the flutter website',
|
||||||
|
(tester) async {
|
||||||
|
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
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 Footer());
|
||||||
|
final flutterTextFinder = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RichText && tapTextSpan(widget, 'Flutter'),
|
||||||
|
);
|
||||||
|
await tester.tap(flutterTextFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
'https://flutter.dev',
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'tapping on "Firebase" opens the firebase website',
|
||||||
|
(tester) async {
|
||||||
|
when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true);
|
||||||
|
when(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
any(),
|
||||||
|
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 Footer());
|
||||||
|
final firebaseTextFinder = find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RichText && tapTextSpan(widget, 'Firebase'),
|
||||||
|
);
|
||||||
|
await tester.tap(firebaseTextFinder);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
verify(
|
||||||
|
() => urlLauncher.launch(
|
||||||
|
'https://firebase.google.com',
|
||||||
|
useSafariVC: any(named: 'useSafariVC'),
|
||||||
|
useWebView: any(named: 'useWebView'),
|
||||||
|
enableJavaScript: any(named: 'enableJavaScript'),
|
||||||
|
enableDomStorage: any(named: 'enableDomStorage'),
|
||||||
|
universalLinksOnly: any(named: 'universalLinksOnly'),
|
||||||
|
headers: any(named: 'headers'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|