mirror of https://github.com/flutter/pinball.git
parent
5edfc2f17a
commit
64a059fd50
@ -0,0 +1,2 @@
|
||||
export 'cubit/assets_manager_cubit.dart';
|
||||
export 'views/views.dart';
|
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:pinball/assets_manager/assets_manager.dart';
|
||||
import 'package:pinball/l10n/l10n.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template assets_loading_page}
|
||||
/// Widget used to indicate the loading progress of the different assets used
|
||||
/// in the game
|
||||
/// {@endtemplate}
|
||||
class AssetsLoadingPage extends StatelessWidget {
|
||||
/// {@macro assets_loading_page}
|
||||
const AssetsLoadingPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final headline1 = Theme.of(context).textTheme.headline1;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'I/O Pinball',
|
||||
style: headline1!.copyWith(fontSize: 80),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
AnimatedEllipsisText(
|
||||
l10n.loading,
|
||||
style: headline1,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
FractionallySizedBox(
|
||||
widthFactor: 0.8,
|
||||
child: BlocBuilder<AssetsManagerCubit, AssetsManagerState>(
|
||||
builder: (context, state) {
|
||||
return PinballLoadingIndicator(value: state.progress);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export 'assets_loading_page.dart';
|
@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@tempalte animated_ellipsis_text}
|
||||
/// Every 500 milliseconds, it will add a new `.` at the end of the given
|
||||
/// [text]. Once 3 `.` have been added (e.g. `Loading...`), it will reset to
|
||||
/// zero ellipsis and start over again.
|
||||
/// {@endtemplate}
|
||||
class AnimatedEllipsisText extends StatefulWidget {
|
||||
/// {@macro animated_ellipsis_text}
|
||||
const AnimatedEllipsisText(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The text that will be animated.
|
||||
final String text;
|
||||
|
||||
/// Optional [TextStyle] of the given [text].
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AnimatedEllipsisText();
|
||||
}
|
||||
|
||||
class _AnimatedEllipsisText extends State<AnimatedEllipsisText>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final Timer timer;
|
||||
var _numberOfEllipsis = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
timer = Timer.periodic(const Duration(milliseconds: 500), (_) {
|
||||
setState(() {
|
||||
_numberOfEllipsis++;
|
||||
_numberOfEllipsis = _numberOfEllipsis % 4;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (timer.isActive) timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
'${widget.text}${_numberOfEllipsis.toEllipsis()}',
|
||||
style: widget.style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on int {
|
||||
String toEllipsis() => '.' * this;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template crt_background}
|
||||
/// [BoxDecoration] that provides a CRT-like background efffect.
|
||||
/// {@endtemplate}
|
||||
class CrtBackground extends BoxDecoration {
|
||||
/// {@macro crt_background}
|
||||
const CrtBackground()
|
||||
: super(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment(1, 0.015),
|
||||
stops: [0.0, 0.5, 0.5, 1],
|
||||
colors: [
|
||||
PinballColors.darkBlue,
|
||||
PinballColors.darkBlue,
|
||||
PinballColors.crtBackground,
|
||||
PinballColors.crtBackground,
|
||||
],
|
||||
tileMode: TileMode.repeated,
|
||||
),
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
/// {@template pinball_loading_indicator}
|
||||
/// Pixel-art loading indicator
|
||||
/// {@endtemplate}
|
||||
class PinballLoadingIndicator extends StatelessWidget {
|
||||
/// {@macro pinball_loading_indicator}
|
||||
const PinballLoadingIndicator({
|
||||
Key? key,
|
||||
required this.value,
|
||||
}) : assert(
|
||||
value >= 0.0 && value <= 1.0,
|
||||
'Progress must be between 0 and 1',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Progress value
|
||||
final double value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_InnerIndicator(value: value, widthFactor: 0.95),
|
||||
_InnerIndicator(value: value, widthFactor: 0.98),
|
||||
_InnerIndicator(value: value),
|
||||
_InnerIndicator(value: value),
|
||||
_InnerIndicator(value: value, widthFactor: 0.98),
|
||||
_InnerIndicator(value: value, widthFactor: 0.95)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InnerIndicator extends StatelessWidget {
|
||||
const _InnerIndicator({
|
||||
Key? key,
|
||||
required this.value,
|
||||
this.widthFactor = 1.0,
|
||||
}) : super(key: key);
|
||||
|
||||
final double value;
|
||||
final double widthFactor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FractionallySizedBox(
|
||||
widthFactor: widthFactor,
|
||||
child: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: PinballColors.loadingDarkBlue,
|
||||
color: PinballColors.loadingDarkRed,
|
||||
value: value,
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: PinballColors.loadingLightBlue,
|
||||
color: PinballColors.loadingLightRed,
|
||||
value: value,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1 +1,4 @@
|
||||
export 'animated_ellipsis_text.dart';
|
||||
export 'crt_background.dart';
|
||||
export 'pinball_button.dart';
|
||||
export 'pinball_loading_indicator.dart';
|
||||
|
@ -0,0 +1,30 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
void main() {
|
||||
group('AnimatedEllipsisText', () {
|
||||
testWidgets(
|
||||
'adds a new `.` every 500ms and '
|
||||
'resets back to zero after adding 3', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AnimatedEllipsisText('test'),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('test'), findsOneWidget);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
expect(find.text('test.'), findsOneWidget);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
expect(find.text('test..'), findsOneWidget);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
expect(find.text('test...'), findsOneWidget);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
expect(find.text('test'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
void main() {
|
||||
group('CrtBackground', () {
|
||||
test('is a BoxDecoration with a LinearGradient', () {
|
||||
// ignore: prefer_const_constructors
|
||||
final crtBg = CrtBackground();
|
||||
const expectedGradient = LinearGradient(
|
||||
begin: Alignment(1, 0.015),
|
||||
stops: [0.0, 0.5, 0.5, 1],
|
||||
colors: [
|
||||
PinballColors.darkBlue,
|
||||
PinballColors.darkBlue,
|
||||
PinballColors.crtBackground,
|
||||
PinballColors.crtBackground,
|
||||
],
|
||||
tileMode: TileMode.repeated,
|
||||
);
|
||||
expect(crtBg, isA<BoxDecoration>());
|
||||
expect(crtBg.gradient, expectedGradient);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
|
||||
void main() {
|
||||
group('PinballLoadingIndicator', () {
|
||||
group('assert value', () {
|
||||
test('throws error if value <= 0.0', () {
|
||||
expect(
|
||||
() => PinballLoadingIndicator(value: -0.5),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws error if value >= 1.0', () {
|
||||
expect(
|
||||
() => PinballLoadingIndicator(value: 1.5),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'renders 12 LinearProgressIndicators and '
|
||||
'6 FractionallySizedBox to indicate progress', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: PinballLoadingIndicator(value: 0.75),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.byType(FractionallySizedBox), findsNWidgets(6));
|
||||
expect(find.byType(LinearProgressIndicator), findsNWidgets(12));
|
||||
final progressIndicators = tester.widgetList<LinearProgressIndicator>(
|
||||
find.byType(LinearProgressIndicator),
|
||||
);
|
||||
for (final i in progressIndicators) {
|
||||
expect(i.value, 0.75);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/assets_manager/assets_manager.dart';
|
||||
|
||||
void main() {
|
||||
group('AssetsManagerState', () {
|
@ -0,0 +1,35 @@
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pinball/assets_manager/cubit/assets_manager_cubit.dart';
|
||||
import 'package:pinball/assets_manager/views/assets_loading_page.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
import '../../helpers/helpers.dart';
|
||||
|
||||
void main() {
|
||||
late AssetsManagerCubit assetsManagerCubit;
|
||||
|
||||
setUp(() {
|
||||
final initialAssetsState = AssetsManagerState(
|
||||
loadables: [Future<void>.value()],
|
||||
loaded: const [],
|
||||
);
|
||||
assetsManagerCubit = MockAssetsManagerCubit();
|
||||
whenListen(
|
||||
assetsManagerCubit,
|
||||
Stream.value(initialAssetsState),
|
||||
initialState: initialAssetsState,
|
||||
);
|
||||
});
|
||||
|
||||
group('AssetsLoadingPage', () {
|
||||
testWidgets('renders an animated text and a pinball loading indicator',
|
||||
(tester) async {
|
||||
await tester.pumpApp(
|
||||
const AssetsLoadingPage(),
|
||||
assetsManagerCubit: assetsManagerCubit,
|
||||
);
|
||||
expect(find.byType(AnimatedEllipsisText), findsOneWidget);
|
||||
expect(find.byType(PinballLoadingIndicator), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import 'package:authentication_repository/authentication_repository.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame_forge2d/flame_forge2d.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:leaderboard_repository/leaderboard_repository.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball/assets_manager/assets_manager.dart';
|
||||
import 'package:pinball/game/game.dart';
|
||||
import 'package:pinball/select_character/select_character.dart';
|
||||
import 'package:pinball/start_game/start_game.dart';
|
||||
import 'package:pinball_audio/pinball_audio.dart';
|
||||
import 'package:pinball_components/pinball_components.dart';
|
||||
import 'package:pinball_ui/pinball_ui.dart';
|
||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||
|
||||
class MockPinballGame extends Mock implements PinballGame {}
|
||||
|
||||
class MockDrain extends Mock implements Drain {}
|
||||
|
||||
class MockBody extends Mock implements Body {}
|
||||
|
||||
class MockBall extends Mock implements Ball {}
|
||||
|
||||
class MockControlledBall extends Mock implements ControlledBall {}
|
||||
|
||||
class MockBallController extends Mock implements BallController {}
|
||||
|
||||
class MockContact extends Mock implements Contact {}
|
||||
|
||||
class MockGameBloc extends Mock implements GameBloc {}
|
||||
|
||||
class MockStartGameBloc extends Mock implements StartGameBloc {}
|
||||
|
||||
class MockGameState extends Mock implements GameState {}
|
||||
|
||||
class MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
|
||||
|
||||
class MockAuthenticationRepository extends Mock
|
||||
implements AuthenticationRepository {}
|
||||
|
||||
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
|
||||
|
||||
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class MockTapDownInfo extends Mock implements TapDownInfo {}
|
||||
|
||||
class MockTapDownDetails extends Mock implements TapDownDetails {}
|
||||
|
||||
class MockTapUpInfo extends Mock implements TapUpInfo {}
|
||||
|
||||
class MockTapUpDetails extends Mock implements TapUpDetails {}
|
||||
|
||||
class MockEventPosition extends Mock implements EventPosition {}
|
||||
|
||||
class MockFilter extends Mock implements Filter {}
|
||||
|
||||
class MockFixture extends Mock implements Fixture {}
|
||||
|
||||
class MockComponent extends Mock implements Component {}
|
||||
|
||||
class MockComponentSet extends Mock implements ComponentSet {}
|
||||
|
||||
class MockDashNestBumper extends Mock implements DashNestBumper {}
|
||||
|
||||
class MockPinballAudio extends Mock implements PinballAudio {}
|
||||
|
||||
class MockSparkyComputerSensor extends Mock implements SparkyComputerSensor {}
|
||||
|
||||
class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}
|
||||
|
||||
class MockBackboard extends Mock implements Backboard {}
|
||||
|
||||
class MockCameraController extends Mock implements CameraController {}
|
||||
|
||||
class MockActiveOverlaysNotifier extends Mock
|
||||
implements ActiveOverlaysNotifier {}
|
||||
|
||||
class MockGameFlowController extends Mock implements GameFlowController {}
|
||||
|
||||
class MockAndroidBumper extends Mock implements AndroidBumper {}
|
||||
|
||||
class MockSparkyBumper extends Mock implements SparkyBumper {}
|
||||
|
||||
class MockMultiballCubit extends Mock implements MultiballCubit {}
|
||||
|
||||
class MockMultiplierCubit extends Mock implements MultiplierCubit {}
|
||||
|
||||
class MockUrlLauncher extends Mock
|
||||
with MockPlatformInterfaceMixin
|
||||
implements UrlLauncherPlatform {}
|
Loading…
Reference in new issue