@ -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,25 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy-dev:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Development
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
- run: flutter packages get
|
||||
- run: flutter build web --target lib/main_development.dart --web-renderer canvaskit --release
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PINBALL_DEV }}"
|
||||
channelId: live
|
||||
projectId: pinball-dev
|
||||
target: ashehwkdkdjruejdnensjsjdne
|
@ -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"
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
@ -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,34 +1,36 @@
|
||||
// ignore_for_file: avoid_renaming_method_parameters
|
||||
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template android_acres}
|
||||
/// Area positioned on the left side of the board containing the [Spaceship],
|
||||
/// [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
|
||||
/// Area positioned on the left side of the board containing the
|
||||
/// [AndroidSpaceship], [SpaceshipRamp], [SpaceshipRail], and [AndroidBumper]s.
|
||||
/// {@endtemplate}
|
||||
class AndroidAcres extends Blueprint {
|
||||
class AndroidAcres extends Component {
|
||||
/// {@macro android_acres}
|
||||
AndroidAcres()
|
||||
: super(
|
||||
components: [
|
||||
children: [
|
||||
SpaceshipRamp(),
|
||||
SpaceshipRail(),
|
||||
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
|
||||
AndroidBumper.a(
|
||||
children: [
|
||||
ScoringBehavior(points: 20),
|
||||
ScoringBehavior(points: 20000),
|
||||
],
|
||||
)..initialPosition = Vector2(-32.52, -9.1),
|
||||
)..initialPosition = Vector2(-25, 1.3),
|
||||
AndroidBumper.b(
|
||||
children: [
|
||||
ScoringBehavior(points: 20000),
|
||||
],
|
||||
)..initialPosition = Vector2(-32.8, -9.2),
|
||||
AndroidBumper.cow(
|
||||
children: [
|
||||
ScoringBehavior(points: 20),
|
||||
],
|
||||
)..initialPosition = Vector2(-22.89, -17.35),
|
||||
],
|
||||
blueprints: [
|
||||
SpaceshipRamp(),
|
||||
Spaceship(position: Vector2(-26.5, -28.5)),
|
||||
SpaceshipRail(),
|
||||
)..initialPosition = Vector2(-20.5, -13.8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,21 +1,20 @@
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:pinball/game/components/components.dart';
|
||||
import 'package:pinball_components/pinball_components.dart' hide Assets;
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template launcher}
|
||||
/// A [Blueprint] which creates the [Plunger], [RocketSpriteComponent] and
|
||||
/// [LaunchRamp].
|
||||
/// Channel on the right side of the board containing the [LaunchRamp],
|
||||
/// [Plunger], and [RocketSpriteComponent].
|
||||
/// {@endtemplate}
|
||||
class Launcher extends Blueprint {
|
||||
class Launcher extends Component {
|
||||
/// {@macro launcher}
|
||||
Launcher()
|
||||
: super(
|
||||
components: [
|
||||
ControlledPlunger(compressionDistance: 14)
|
||||
..initialPosition = Vector2(40.7, 38),
|
||||
RocketSpriteComponent()..position = Vector2(43, 62),
|
||||
children: [
|
||||
LaunchRamp(),
|
||||
ControlledPlunger(compressionDistance: 10.5)
|
||||
..initialPosition = Vector2(41.1, 43),
|
||||
RocketSpriteComponent()..position = Vector2(43, 62.3),
|
||||
],
|
||||
blueprints: [LaunchRamp()],
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
export 'multipliers_behavior.dart';
|
@ -0,0 +1,25 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_bloc/flame_bloc.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// Toggle each [Multiplier] when GameState.multiplier changes.
|
||||
class MultipliersBehavior extends Component
|
||||
with
|
||||
HasGameRef<PinballGame>,
|
||||
ParentIsA<Multipliers>,
|
||||
BlocComponent<GameBloc, GameState> {
|
||||
@override
|
||||
bool listenWhen(GameState? previousState, GameState newState) {
|
||||
return previousState?.multiplier != newState.multiplier;
|
||||
}
|
||||
|
||||
@override
|
||||
void onNewState(GameState state) {
|
||||
final multipliers = parent.children.whereType<Multiplier>();
|
||||
for (final multiplier in multipliers) {
|
||||
multiplier.bloc.next(state.multiplier);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/game/components/multipliers/behaviors/behaviors.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_flame/pinball_flame.dart';
|
||||
|
||||
/// {@template multipliers}
|
||||
/// A group for the multipliers on the board.
|
||||
/// {@endtemplate}
|
||||
class Multipliers extends Component with ZIndex {
|
||||
/// {@macro multipliers}
|
||||
Multipliers()
|
||||
: super(
|
||||
children: [
|
||||
Multiplier.x2(
|
||||
position: Vector2(-19.5, -2),
|
||||
angle: -15 * math.pi / 180,
|
||||
),
|
||||
Multiplier.x3(
|
||||
position: Vector2(13, -9.4),
|
||||
angle: 15 * math.pi / 180,
|
||||
),
|
||||
Multiplier.x4(
|
||||
position: Vector2(0, -21.2),
|
||||
angle: 0,
|
||||
),
|
||||
Multiplier.x5(
|
||||
position: Vector2(-8.5, -28),
|
||||
angle: -3 * math.pi / 180,
|
||||
),
|
||||
Multiplier.x6(
|
||||
position: Vector2(10, -30.7),
|
||||
angle: 8 * math.pi / 180,
|
||||
),
|
||||
MultipliersBehavior(),
|
||||
],
|
||||
) {
|
||||
zIndex = ZIndexes.decal;
|
||||
}
|
||||
|
||||
/// Creates [Multipliers] without any children.
|
||||
///
|
||||
/// This can be used for testing [Multipliers]'s behaviors in isolation.
|
||||
@visibleForTesting
|
||||
Multipliers.test();
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'widgets/widgets.dart';
|
@ -0,0 +1,305 @@
|
||||
// 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_ui/pinball_ui.dart';
|
||||
import 'package:platform_helper/platform_helper.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showHowToPlayDialog(BuildContext context) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => HowToPlayDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
class HowToPlayDialog extends StatefulWidget {
|
||||
HowToPlayDialog({
|
||||
Key? key,
|
||||
@visibleForTesting PlatformHelper? platformHelper,
|
||||
}) : platformHelper = platformHelper ?? PlatformHelper(),
|
||||
super(key: key);
|
||||
|
||||
final PlatformHelper platformHelper;
|
||||
|
||||
@override
|
||||
State<HowToPlayDialog> createState() => _HowToPlayDialogState();
|
||||
}
|
||||
|
||||
class _HowToPlayDialogState extends State<HowToPlayDialog> {
|
||||
late Timer closeTimer;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
closeTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
closeTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = widget.platformHelper.isMobile;
|
||||
final l10n = context.l10n;
|
||||
return PinballDialog(
|
||||
title: l10n.howToPlay,
|
||||
subtitle: l10n.tipsForFlips,
|
||||
child: 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);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final headline3 = Theme.of(context)
|
||||
.textTheme
|
||||
.headline3!
|
||||
.copyWith(color: PinballColors.white);
|
||||
return Column(
|
||||
children: [
|
||||
Text(l10n.tapAndHoldRocket, style: headline3),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: '${l10n.to} ', style: headline3),
|
||||
TextSpan(
|
||||
text: l10n.launch,
|
||||
style: headline3.copyWith(color: PinballColors.blue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileFlipperControls extends StatelessWidget {
|
||||
const _MobileFlipperControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final headline3 = Theme.of(context)
|
||||
.textTheme
|
||||
.headline3!
|
||||
.copyWith(color: PinballColors.white);
|
||||
return Column(
|
||||
children: [
|
||||
Text(l10n.tapLeftRightScreen, style: headline3),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: '${l10n.to} ', style: headline3),
|
||||
TextSpan(
|
||||
text: l10n.flip,
|
||||
style: headline3.copyWith(color: PinballColors.orange),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopBody extends StatelessWidget {
|
||||
const _DesktopBody({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: const [
|
||||
SizedBox(height: 16),
|
||||
_DesktopLaunchControls(),
|
||||
SizedBox(height: 16),
|
||||
_DesktopFlipperControls(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopLaunchControls extends StatelessWidget {
|
||||
const _DesktopLaunchControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.launchControls,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
children: const [
|
||||
_KeyButton(control: Control.down),
|
||||
SizedBox(width: 10),
|
||||
_KeyButton(control: Control.space),
|
||||
SizedBox(width: 10),
|
||||
_KeyButton(control: Control.s),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopFlipperControls extends StatelessWidget {
|
||||
const _DesktopFlipperControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.flipperControls,
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
_KeyButton(control: Control.left),
|
||||
SizedBox(width: 20),
|
||||
_KeyButton(control: Control.right),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
children: const [
|
||||
_KeyButton(control: Control.a),
|
||||
SizedBox(width: 20),
|
||||
_KeyButton(control: Control.d),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyButton extends StatelessWidget {
|
||||
const _KeyButton({Key? key, required this.control}) : super(key: key);
|
||||
|
||||
final Control control;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final textStyle =
|
||||
control.isArrow ? textTheme.headline1 : textTheme.headline3;
|
||||
const height = 60.0;
|
||||
final width = control.isSpace ? height * 2.83 : height;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.fill,
|
||||
image: AssetImage(
|
||||
control.isSpace
|
||||
? Assets.images.components.space.keyName
|
||||
: Assets.images.components.key.keyName,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: height,
|
||||
child: Center(
|
||||
child: RotatedBox(
|
||||
quarterTurns: control.isDown ? 1 : 0,
|
||||
child: Text(
|
||||
control.getCharacter(context),
|
||||
style: textStyle?.copyWith(color: PinballColors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,71 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
|
||||
part 'leaderboard_event.dart';
|
||||
part 'leaderboard_state.dart';
|
||||
|
||||
/// {@template leaderboard_bloc}
|
||||
/// Manages leaderboard events.
|
||||
///
|
||||
/// Uses a [LeaderboardRepository] to request and update players participations.
|
||||
/// {@endtemplate}
|
||||
class LeaderboardBloc extends Bloc<LeaderboardEvent, LeaderboardState> {
|
||||
/// {@macro leaderboard_bloc}
|
||||
LeaderboardBloc(this._leaderboardRepository)
|
||||
: super(const LeaderboardState.initial()) {
|
||||
on<Top10Fetched>(_onTop10Fetched);
|
||||
on<LeaderboardEntryAdded>(_onLeaderboardEntryAdded);
|
||||
}
|
||||
|
||||
final LeaderboardRepository _leaderboardRepository;
|
||||
|
||||
Future<void> _onTop10Fetched(
|
||||
Top10Fetched event,
|
||||
Emitter<LeaderboardState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: LeaderboardStatus.loading));
|
||||
try {
|
||||
final top10Leaderboard =
|
||||
await _leaderboardRepository.fetchTop10Leaderboard();
|
||||
|
||||
final leaderboardEntries = <LeaderboardEntry>[];
|
||||
top10Leaderboard.asMap().forEach(
|
||||
(index, value) => leaderboardEntries.add(value.toEntry(index + 1)),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: LeaderboardStatus.success,
|
||||
leaderboard: leaderboardEntries,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
emit(state.copyWith(status: LeaderboardStatus.error));
|
||||
addError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLeaderboardEntryAdded(
|
||||
LeaderboardEntryAdded event,
|
||||
Emitter<LeaderboardState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: LeaderboardStatus.loading));
|
||||
try {
|
||||
final ranking =
|
||||
await _leaderboardRepository.addLeaderboardEntry(event.entry);
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: LeaderboardStatus.success,
|
||||
ranking: ranking,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
emit(state.copyWith(status: LeaderboardStatus.error));
|
||||
addError(error);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
part of 'leaderboard_bloc.dart';
|
||||
|
||||
/// {@template leaderboard_event}
|
||||
/// Represents the events available for [LeaderboardBloc].
|
||||
/// {endtemplate}
|
||||
abstract class LeaderboardEvent extends Equatable {
|
||||
/// {@macro leaderboard_event}
|
||||
const LeaderboardEvent();
|
||||
}
|
||||
|
||||
/// {@template top_10_fetched}
|
||||
/// Request the top 10 [LeaderboardEntryData]s.
|
||||
/// {endtemplate}
|
||||
class Top10Fetched extends LeaderboardEvent {
|
||||
/// {@macro top_10_fetched}
|
||||
const Top10Fetched();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// {@template leaderboard_entry_added}
|
||||
/// Writes a new [LeaderboardEntryData].
|
||||
///
|
||||
/// Should be added when a player finishes a game.
|
||||
/// {endtemplate}
|
||||
class LeaderboardEntryAdded extends LeaderboardEvent {
|
||||
/// {@macro leaderboard_entry_added}
|
||||
const LeaderboardEntryAdded({required this.entry});
|
||||
|
||||
/// [LeaderboardEntryData] to be written to the remote storage.
|
||||
final LeaderboardEntryData entry;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [entry];
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
part of 'leaderboard_bloc.dart';
|
||||
|
||||
/// Defines the request status.
|
||||
enum LeaderboardStatus {
|
||||
/// Request is being loaded.
|
||||
loading,
|
||||
|
||||
/// Request was processed successfully and received a valid response.
|
||||
success,
|
||||
|
||||
/// Request was processed unsuccessfully and received an error.
|
||||
error,
|
||||
}
|
||||
|
||||
/// {@template leaderboard_state}
|
||||
/// Represents the state of the leaderboard.
|
||||
/// {@endtemplate}
|
||||
class LeaderboardState extends Equatable {
|
||||
/// {@macro leaderboard_state}
|
||||
const LeaderboardState({
|
||||
required this.status,
|
||||
required this.ranking,
|
||||
required this.leaderboard,
|
||||
});
|
||||
|
||||
const LeaderboardState.initial()
|
||||
: status = LeaderboardStatus.loading,
|
||||
ranking = const LeaderboardRanking(
|
||||
ranking: 0,
|
||||
outOf: 0,
|
||||
),
|
||||
leaderboard = const [];
|
||||
|
||||
/// The current [LeaderboardStatus] of the state.
|
||||
final LeaderboardStatus status;
|
||||
|
||||
/// Rank of the current player.
|
||||
final LeaderboardRanking ranking;
|
||||
|
||||
/// List of top-ranked players.
|
||||
final List<LeaderboardEntry> leaderboard;
|
||||
|
||||
@override
|
||||
List<Object> get props => [status, ranking, leaderboard];
|
||||
|
||||
LeaderboardState copyWith({
|
||||
LeaderboardStatus? status,
|
||||
LeaderboardRanking? ranking,
|
||||
List<LeaderboardEntry>? leaderboard,
|
||||
}) {
|
||||
return LeaderboardState(
|
||||
status: status ?? this.status,
|
||||
ranking: ranking ?? this.ranking,
|
||||
leaderboard: leaderboard ?? this.leaderboard,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export 'bloc/leaderboard_bloc.dart';
|
||||
export 'models/leader_board_entry.dart';
|
||||
export 'view/leaderboard_page.dart';
|
@ -1,306 +0,0 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball/leaderboard/leaderboard.dart';
|
||||
import 'package:pinball/select_character/select_character.dart';
|
||||
import 'package:pinball_theme/pinball_theme.dart';
|
||||
|
||||
class LeaderboardPage extends StatelessWidget {
|
||||
const LeaderboardPage({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
static Route route({required CharacterTheme theme}) {
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => LeaderboardPage(theme: theme),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => LeaderboardBloc(
|
||||
context.read<LeaderboardRepository>(),
|
||||
)..add(const Top10Fetched()),
|
||||
child: LeaderboardView(theme: theme),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LeaderboardView extends StatelessWidget {
|
||||
const LeaderboardView({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Text(
|
||||
l10n.leaderboard,
|
||||
style: Theme.of(context).textTheme.headline3,
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
BlocBuilder<LeaderboardBloc, LeaderboardState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case LeaderboardStatus.loading:
|
||||
return _LeaderboardLoading(theme: theme);
|
||||
case LeaderboardStatus.success:
|
||||
return _LeaderboardRanking(
|
||||
ranking: state.leaderboard,
|
||||
theme: theme,
|
||||
);
|
||||
case LeaderboardStatus.error:
|
||||
return _LeaderboardError(theme: theme);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).push<void>(
|
||||
CharacterSelectionDialog.route(),
|
||||
),
|
||||
child: Text(l10n.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardLoading extends StatelessWidget {
|
||||
const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardError extends StatelessWidget {
|
||||
const _LeaderboardError({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Text(
|
||||
'There was en error loading data!',
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardRanking extends StatelessWidget {
|
||||
const _LeaderboardRanking({
|
||||
Key? key,
|
||||
required this.ranking,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<LeaderboardEntry> ranking;
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardHeaders(theme: theme),
|
||||
_LeaderboardList(
|
||||
ranking: ranking,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardHeaders extends StatelessWidget {
|
||||
const _LeaderboardHeaders({Key? key, required this.theme}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardHeaderItem(title: l10n.rank, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.character, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.username, theme: theme),
|
||||
_LeaderboardHeaderItem(title: l10n.score, theme: theme),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardHeaderItem extends StatelessWidget {
|
||||
const _LeaderboardHeaderItem({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.ballColor,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardList extends StatelessWidget {
|
||||
const _LeaderboardList({
|
||||
Key? key,
|
||||
required this.ranking,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<LeaderboardEntry> ranking;
|
||||
final CharacterTheme theme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (_, index) => _LeaderBoardCompetitor(
|
||||
entry: ranking[index],
|
||||
theme: theme,
|
||||
),
|
||||
itemCount: ranking.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderBoardCompetitor extends StatelessWidget {
|
||||
const _LeaderBoardCompetitor({
|
||||
Key? key,
|
||||
required this.entry,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
|
||||
final LeaderboardEntry entry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.rank,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorCharacter(
|
||||
characterAsset: entry.character,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.playerInitials,
|
||||
theme: theme,
|
||||
),
|
||||
_LeaderboardCompetitorField(
|
||||
text: entry.score.toString(),
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardCompetitorField extends StatelessWidget {
|
||||
const _LeaderboardCompetitorField({
|
||||
Key? key,
|
||||
required this.text,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.ballColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(text),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeaderboardCompetitorCharacter extends StatelessWidget {
|
||||
const _LeaderboardCompetitorCharacter({
|
||||
Key? key,
|
||||
required this.characterAsset,
|
||||
required this.theme,
|
||||
}) : super(key: key);
|
||||
|
||||
final CharacterTheme theme;
|
||||
final AssetGenImage characterAsset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: theme.ballColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: characterAsset.image(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
class HowToPlayDialog extends StatelessWidget {
|
||||
const HowToPlayDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
const spacing = SizedBox(height: 16);
|
||||
|
||||
return PixelatedDecoration(
|
||||
header: Text(l10n.howToPlay),
|
||||
body: ListView(
|
||||
children: const [
|
||||
spacing,
|
||||
_LaunchControls(),
|
||||
spacing,
|
||||
_FlipperControls(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LaunchControls extends StatelessWidget {
|
||||
const _LaunchControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
const spacing = SizedBox(width: 10);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(l10n.launchControls),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
children: const [
|
||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_down),
|
||||
spacing,
|
||||
KeyIndicator.fromKeyName(keyName: 'SPACE'),
|
||||
spacing,
|
||||
KeyIndicator.fromKeyName(keyName: 'S'),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FlipperControls extends StatelessWidget {
|
||||
const _FlipperControls({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
const rowSpacing = SizedBox(width: 20);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(l10n.flipperControls),
|
||||
const SizedBox(height: 10),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_left),
|
||||
rowSpacing,
|
||||
KeyIndicator.fromIcon(keyIcon: Icons.keyboard_arrow_right),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
children: const [
|
||||
KeyIndicator.fromKeyName(keyName: 'A'),
|
||||
rowSpacing,
|
||||
KeyIndicator.fromKeyName(keyName: 'D'),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(allisonryan0002): remove visibility when adding final UI.
|
||||
@visibleForTesting
|
||||
class KeyIndicator extends StatelessWidget {
|
||||
const KeyIndicator._({
|
||||
Key? key,
|
||||
required String keyName,
|
||||
required IconData keyIcon,
|
||||
required bool fromIcon,
|
||||
}) : _keyName = keyName,
|
||||
_keyIcon = keyIcon,
|
||||
_fromIcon = fromIcon,
|
||||
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;
|
||||
|
||||
@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);
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(
|
||||
color: boarderColor,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: _fromIcon
|
||||
? Padding(
|
||||
padding: iconPadding,
|
||||
child: Icon(_keyIcon, color: color),
|
||||
)
|
||||
: Padding(
|
||||
padding: textPadding,
|
||||
child: Text(_keyName, style: TextStyle(color: color)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/// {@template leaderboard_exception}
|
||||
/// Base exception for leaderboard repository failures.
|
||||
/// {@endtemplate}
|
||||
abstract class LeaderboardException implements Exception {
|
||||
/// {@macro leaderboard_exception}
|
||||
const LeaderboardException(this.error, this.stackTrace);
|
||||
|
||||
/// The error that was caught.
|
||||
final Object error;
|
||||
|
||||
/// The Stacktrace associated with the [error].
|
||||
final StackTrace stackTrace;
|
||||
}
|
||||
|
||||
/// {@template leaderboard_deserialization_exception}
|
||||
/// Exception thrown when leaderboard data cannot be deserialized in the
|
||||
/// expected way.
|
||||
/// {@endtemplate}
|
||||
class LeaderboardDeserializationException extends LeaderboardException {
|
||||
/// {@macro leaderboard_deserialization_exception}
|
||||
const LeaderboardDeserializationException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
||||
|
||||
/// {@template fetch_top_10_leaderboard_exception}
|
||||
/// Exception thrown when failure occurs while fetching top 10 leaderboard.
|
||||
/// {@endtemplate}
|
||||
class FetchTop10LeaderboardException extends LeaderboardException {
|
||||
/// {@macro fetch_top_10_leaderboard_exception}
|
||||
const FetchTop10LeaderboardException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
||||
|
||||
/// {@template fetch_leaderboard_exception}
|
||||
/// Exception thrown when failure occurs while fetching the leaderboard.
|
||||
/// {@endtemplate}
|
||||
class FetchLeaderboardException extends LeaderboardException {
|
||||
/// {@macro fetch_top_10_leaderboard_exception}
|
||||
const FetchLeaderboardException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
||||
|
||||
/// {@template delete_leaderboard_exception}
|
||||
/// Exception thrown when failure occurs while deleting the leaderboard under
|
||||
/// the tenth position.
|
||||
/// {@endtemplate}
|
||||
class DeleteLeaderboardException extends LeaderboardException {
|
||||
/// {@macro fetch_top_10_leaderboard_exception}
|
||||
const DeleteLeaderboardException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
||||
|
||||
/// {@template add_leaderboard_entry_exception}
|
||||
/// Exception thrown when failure occurs while adding entry to leaderboard.
|
||||
/// {@endtemplate}
|
||||
class AddLeaderboardEntryException extends LeaderboardException {
|
||||
/// {@macro add_leaderboard_entry_exception}
|
||||
const AddLeaderboardEntryException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
||||
|
||||
/// {@template fetch_prohibited_initials_exception}
|
||||
/// Exception thrown when failure occurs while fetching prohibited initials.
|
||||
/// {@endtemplate}
|
||||
class FetchProhibitedInitialsException extends LeaderboardException {
|
||||
/// {@macro fetch_prohibited_initials_exception}
|
||||
const FetchProhibitedInitialsException(Object error, StackTrace stackTrace)
|
||||
: super(error, stackTrace);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
|
||||
/// {@template leaderboard_ranking}
|
||||
/// Contains [ranking] for a single [LeaderboardEntryData] and the number of
|
||||
/// players the [ranking] is [outOf].
|
||||
/// {@endtemplate}
|
||||
class LeaderboardRanking extends Equatable {
|
||||
/// {@macro leaderboard_ranking}
|
||||
const LeaderboardRanking({required this.ranking, required this.outOf});
|
||||
|
||||
/// Place ranking by score for a [LeaderboardEntryData].
|
||||
final int ranking;
|
||||
|
||||
/// Number of [LeaderboardEntryData]s at the time of score entry.
|
||||
final int outOf;
|
||||
|
||||
@override
|
||||
List<Object> get props => [ranking, outOf];
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('LeaderboardRanking', () {
|
||||
test('can be instantiated', () {
|
||||
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||
|
||||
expect(leaderboardRanking, isNotNull);
|
||||
});
|
||||
|
||||
test('supports value equality.', () {
|
||||
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||
const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1);
|
||||
|
||||
expect(leaderboardRanking, equals(leaderboardRanking2));
|
||||
});
|
||||
});
|
||||
}
|
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 |
After 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: 28 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 231 KiB |