feat: props selection (#245)

* feat: hat selection

* feat: reutilize code

* feat: selected glasses

* chore: coverage

* chore: on tab

* feat: glasses selected

* feat: clothes selection

* feat: other selection

* chore: rename

* chore: rename

* chore: rename

* chore: rename

* chore: refactor enums

* chore: feedback

* chore: comment

* chore: semantics

* chore: keys

* chore: refactor

* chore: rename

* chore: nit

* Update lib/in_experience_selection/widgets/props_selection_tab_bar_view.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* Update lib/in_experience_selection/options/glasses.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
This commit is contained in:
Oscar
2022-12-12 14:32:19 +01:00
committed by GitHub
parent 7316cc89ff
commit d503c7fe85
42 changed files with 859 additions and 80 deletions

BIN
assets/props/clothes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

BIN
assets/props/hats_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

View File

@ -170,11 +170,27 @@ class $AssetsImagesGen {
class $AssetsPropsGen {
const $AssetsPropsGen();
/// File path: assets/props/clothes.png
AssetGenImage get clothes => const AssetGenImage('assets/props/clothes.png');
/// File path: assets/props/glasses_icon.png
AssetGenImage get glassesIcon =>
const AssetGenImage('assets/props/glasses_icon.png');
/// File path: assets/props/hats_icon.png
AssetGenImage get hatsIcon =>
const AssetGenImage('assets/props/hats_icon.png');
/// File path: assets/props/others_icon.png
AssetGenImage get othersIcon =>
const AssetGenImage('assets/props/others_icon.png');
/// File path: assets/props/prop1.png
AssetGenImage get prop1 => const AssetGenImage('assets/props/prop1.png');
/// List of all assets
List<AssetGenImage> get values => [prop1];
List<AssetGenImage> get values =>
[clothes, glassesIcon, hatsIcon, othersIcon, prop1];
}
class Assets {

View File

@ -1,8 +1,8 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
part 'character_selection_event.dart';
part 'character_selection_state.dart';
class CharacterSelectionBloc extends Bloc<CharacterSelectionEvent, Character> {
CharacterSelectionBloc() : super(Character.dash) {

View File

@ -1,6 +0,0 @@
part of 'character_selection_bloc.dart';
enum Character {
dash,
sparky,
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/assets/assets.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class CharacterSelector extends StatefulWidget {
const CharacterSelector({super.key, required this.viewportFraction});

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
part '../../in_experience_selection/bloc/in_experience_selection_event.dart';
@ -12,19 +11,22 @@ class InExperienceSelectionBloc
extends Bloc<InExperienceSelectionEvent, InExperienceSelectionState> {
InExperienceSelectionBloc({required Character characterPreSelected})
: super(InExperienceSelectionState(character: characterPreSelected)) {
on<InExperienceSelectionHatSelected>(_hatSelected);
on<InExperienceSelectionHatToggled>(_hatToggled);
on<InExperienceSelectionBackgroundSelected>(_backgroundSelected);
on<InExperienceSelectionCharacterSelected>(_characterSelected);
on<InExperienceSelectionGlassesToggled>(_glassesToggled);
on<InExperienceSelectionClothesToggled>(_clothesToggled);
on<InExperienceSelectionHandleheldLeftToggled>(_handleheldLeftToggled);
}
FutureOr<void> _hatSelected(
InExperienceSelectionHatSelected event,
FutureOr<void> _hatToggled(
InExperienceSelectionHatToggled event,
Emitter<InExperienceSelectionState> emit,
) {
if (event.hat == state.selectedHat) {
emit(state.copyWith(selectedHat: Hats.none));
if (event.hat == state.hat) {
emit(state.copyWith(hat: Hats.none));
} else {
emit(state.copyWith(selectedHat: event.hat));
emit(state.copyWith(hat: event.hat));
}
}
@ -41,4 +43,37 @@ class InExperienceSelectionBloc
) {
emit(state.copyWith(character: event.character));
}
FutureOr<void> _glassesToggled(
InExperienceSelectionGlassesToggled event,
Emitter<InExperienceSelectionState> emit,
) {
if (event.glasses == state.glasses) {
emit(state.copyWith(glasses: Glasses.none));
} else {
emit(state.copyWith(glasses: event.glasses));
}
}
FutureOr<void> _clothesToggled(
InExperienceSelectionClothesToggled event,
Emitter<InExperienceSelectionState> emit,
) {
if (event.clothes == state.clothes) {
emit(state.copyWith(clothes: Clothes.none));
} else {
emit(state.copyWith(clothes: event.clothes));
}
}
FutureOr<void> _handleheldLeftToggled(
InExperienceSelectionHandleheldLeftToggled event,
Emitter<InExperienceSelectionState> emit,
) {
if (event.handheldlLeft == state.handheldlLeft) {
emit(state.copyWith(handheldlLeft: HandheldlLeft.none));
} else {
emit(state.copyWith(handheldlLeft: event.handheldlLeft));
}
}
}

View File

@ -4,8 +4,8 @@ abstract class InExperienceSelectionEvent extends Equatable {
const InExperienceSelectionEvent();
}
class InExperienceSelectionHatSelected extends InExperienceSelectionEvent {
const InExperienceSelectionHatSelected(this.hat);
class InExperienceSelectionHatToggled extends InExperienceSelectionEvent {
const InExperienceSelectionHatToggled(this.hat);
final Hats hat;
@ -32,3 +32,31 @@ class InExperienceSelectionCharacterSelected
@override
List<Object> get props => [character];
}
class InExperienceSelectionGlassesToggled extends InExperienceSelectionEvent {
const InExperienceSelectionGlassesToggled(this.glasses);
final Glasses glasses;
@override
List<Object> get props => [glasses];
}
class InExperienceSelectionClothesToggled extends InExperienceSelectionEvent {
const InExperienceSelectionClothesToggled(this.clothes);
final Clothes clothes;
@override
List<Object> get props => [clothes];
}
class InExperienceSelectionHandleheldLeftToggled
extends InExperienceSelectionEvent {
const InExperienceSelectionHandleheldLeftToggled(this.handheldlLeft);
final HandheldlLeft handheldlLeft;
@override
List<Object> get props => [handheldlLeft];
}

View File

@ -2,31 +2,46 @@ part of 'in_experience_selection_bloc.dart';
class InExperienceSelectionState extends Equatable {
const InExperienceSelectionState({
this.selectedHat = Hats.none,
this.hat = Hats.none,
this.background = Background.space,
this.character = Character.dash,
this.glasses = Glasses.none,
this.clothes = Clothes.none,
this.handheldlLeft = HandheldlLeft.none,
});
final Hats selectedHat;
final Hats hat;
final Background background;
final Character character;
final Glasses glasses;
final Clothes clothes;
final HandheldlLeft handheldlLeft;
@override
List<Object?> get props => [
selectedHat,
hat,
background,
character,
glasses,
clothes,
handheldlLeft,
];
InExperienceSelectionState copyWith({
Hats? selectedHat,
Hats? hat,
Background? background,
Character? character,
Glasses? glasses,
Clothes? clothes,
HandheldlLeft? handheldlLeft,
}) {
return InExperienceSelectionState(
selectedHat: selectedHat ?? this.selectedHat,
hat: hat ?? this.hat,
background: background ?? this.background,
character: character ?? this.character,
glasses: glasses ?? this.glasses,
clothes: clothes ?? this.clothes,
handheldlLeft: handheldlLeft ?? this.handheldlLeft,
);
}
}

View File

@ -1,19 +1,13 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/assets/assets.dart';
enum Background { space, beach, underwater }
enum Background {
space(1),
beach(2),
underwater(3);
extension BackgroundX on Background {
double toDouble() {
switch (this) {
case Background.space:
return 1;
case Background.beach:
return 2;
case Background.underwater:
return 3;
}
}
const Background(this.riveIndex);
final double riveIndex;
ImageProvider toImageProvider() {
switch (this) {

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/assets/assets.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
extension CharacterX on Character {
enum Character {
dash,
sparky;
Image toImage() {
switch (this) {
case Character.dash:

View File

@ -0,0 +1,5 @@
enum Clothes {
none,
clothes1,
clothes2,
}

View File

@ -0,0 +1,8 @@
enum Glasses {
none(0),
glasses1(2);
const Glasses(this.riveIndex);
final double riveIndex;
}

View File

@ -0,0 +1,12 @@
enum HandheldlLeft {
none,
handheldLeft1,
handheldLeft2,
handheldLeft3,
handheldLeft4,
handheldLeft5,
handheldLeft6,
handheldLeft7,
handheldLeft8,
handheldLeft9,
}

View File

@ -1,3 +1,6 @@
export 'background.dart';
export 'character.dart';
export 'clothes.dart';
export 'glasses.dart';
export 'handhelf_left.dart';
export 'hats.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:photobooth_ui/photobooth_ui.dart';

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class ClothesSelectionTabBarView extends StatelessWidget {
const ClothesSelectionTabBarView({super.key});
@visibleForTesting
static Key clothesSelectionKey(Clothes item) {
return Key('clothes_selection_${item.name}');
}
@override
Widget build(BuildContext context) {
final selectedClothes =
context.select((InExperienceSelectionBloc bloc) => bloc.state.clothes);
const items = Clothes.values;
return PropsGridView(
itemBuilder: (context, index) {
final item = items[index];
return PropSelectionElement(
key: clothesSelectionKey(item),
onTap: () {
context
.read<InExperienceSelectionBloc>()
.add(InExperienceSelectionClothesToggled(item));
},
name: item.name,
isSelected: item == selectedClothes,
);
},
itemCount: items.length,
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class GlassesSelectionTabBarView extends StatelessWidget {
const GlassesSelectionTabBarView({super.key});
@visibleForTesting
static Key glassesSelectionKey(Glasses item) {
return Key('glasses_selection_${item.name}');
}
@override
Widget build(BuildContext context) {
final selectedGlasses =
context.select((InExperienceSelectionBloc bloc) => bloc.state.glasses);
const items = Glasses.values;
return PropsGridView(
itemBuilder: (context, index) {
final item = items[index];
return PropSelectionElement(
key: glassesSelectionKey(item),
onTap: () {
context
.read<InExperienceSelectionBloc>()
.add(InExperienceSelectionGlassesToggled(item));
},
name: item.name,
isSelected: item == selectedGlasses,
);
},
itemCount: items.length,
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class HatsSelectionTabBarView extends StatelessWidget {
const HatsSelectionTabBarView({super.key});
@visibleForTesting
static Key hatSelectionKey(Hats item) {
return Key('hat_selection_${item.name}');
}
@override
Widget build(BuildContext context) {
final selectedHat =
context.select((InExperienceSelectionBloc bloc) => bloc.state.hat);
return PropsGridView(
itemBuilder: (context, index) {
final item = Hats.values[index];
return PropSelectionElement(
key: hatSelectionKey(item),
onTap: () {
context
.read<InExperienceSelectionBloc>()
.add(InExperienceSelectionHatToggled(item));
},
name: item.name,
isSelected: item == selectedHat,
);
},
itemCount: Hats.values.length,
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class MiscellaneousSelectionTabBarView extends StatelessWidget {
const MiscellaneousSelectionTabBarView({super.key});
@visibleForTesting
static Key miscellaneousSelectionKey(HandheldlLeft item) {
return Key('miscellaneous_selection_${item.name}');
}
@override
Widget build(BuildContext context) {
final selectedHandheldlLeft = context
.select((InExperienceSelectionBloc bloc) => bloc.state.handheldlLeft);
const items = HandheldlLeft.values;
return PropsGridView(
itemBuilder: (context, index) {
final item = items[index];
return PropSelectionElement(
key: miscellaneousSelectionKey(item),
onTap: () {
context
.read<InExperienceSelectionBloc>()
.add(InExperienceSelectionHandleheldLeftToggled(item));
},
name: item.name,
isSelected: item == selectedHandheldlLeft,
);
},
itemCount: items.length,
);
}
}

View File

@ -58,7 +58,7 @@ class _PrimarySelectionViewState extends State<PrimarySelectionView>
children: const [
CharacterSelectionTabBarView(),
BackgroundSelectionTabBarView(),
SizedBox(),
PropsSelectionTabBarView(),
],
),
),

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class PropsGridView extends StatelessWidget {
const PropsGridView({
super.key,
required this.itemBuilder,
required this.itemCount,
});
final Widget? Function(BuildContext context, int index) itemBuilder;
final int itemCount;
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: itemCount,
itemBuilder: itemBuilder,
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class PropSelectionElement extends StatelessWidget {
const PropSelectionElement({
required this.name,
required this.isSelected,
required this.onTap,
super.key,
});
final String name;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Semantics(
focusable: true,
button: true,
label: name,
child: InkWell(
onTap: onTap,
child: Container(
height: 75,
width: 100,
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected
? PhotoboothColors.white
: PhotoboothColors.transparent,
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFF2C2C2C).withOpacity(0.3),
const Color(0xFF868686).withOpacity(0.4),
],
),
),
child: Text(
name,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: PhotoboothColors.white),
),
),
),
);
}
}

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/assets/assets.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
class PropsSelectionTabBarView extends StatefulWidget {
const PropsSelectionTabBarView({
super.key,
this.initialIndex = 0,
});
@visibleForTesting
static const glassesTabKey = ValueKey('glasses_tab');
@visibleForTesting
static const clothesTabKey = ValueKey('clothes_tab');
@visibleForTesting
static const othersTabKey = ValueKey('others_tab');
final int initialIndex;
@override
State<PropsSelectionTabBarView> createState() =>
_PropsSelectionTabBarViewState();
}
class _PropsSelectionTabBarViewState extends State<PropsSelectionTabBarView>
with TickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(
length: 4,
vsync: this,
initialIndex: widget.initialIndex,
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TabBar(
controller: _tabController,
tabs: [
_PropSelectionTab(
assetGenImage: Assets.props.hatsIcon,
),
_PropSelectionTab(
key: PropsSelectionTabBarView.glassesTabKey,
assetGenImage: Assets.props.glassesIcon,
),
_PropSelectionTab(
key: PropsSelectionTabBarView.clothesTabKey,
assetGenImage: Assets.props.clothes,
),
_PropSelectionTab(
key: PropsSelectionTabBarView.othersTabKey,
assetGenImage: Assets.props.othersIcon,
),
],
),
const Divider(),
Expanded(
child: TabBarView(
controller: _tabController,
children: const [
HatsSelectionTabBarView(),
GlassesSelectionTabBarView(),
ClothesSelectionTabBarView(),
MiscellaneousSelectionTabBarView(),
],
),
),
],
);
}
}
class _PropSelectionTab extends StatefulWidget {
const _PropSelectionTab({
super.key,
required this.assetGenImage,
});
final AssetGenImage assetGenImage;
@override
State<_PropSelectionTab> createState() => _PropSelectionTabState();
}
class _PropSelectionTabState extends State<_PropSelectionTab>
with AutomaticKeepAliveClientMixin<_PropSelectionTab> {
@override
Widget build(BuildContext context) {
super.build(context);
// Setting default icon size to avoid tap issues on testing.
// As the child will be an image, if there is no default size, on tap will
// throw a warning because the child will have no size
final iconSize = IconTheme.of(context).size;
return Tab(
child: widget.assetGenImage.image(
color: IconTheme.of(context).color,
height: iconSize,
width: iconSize,
),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@ -1,4 +1,11 @@
export 'background_selection_tab_bar_view.dart';
export 'character_selection_tab_bar_view.dart';
export 'clothes_selection_tab_bar_view.dart';
export 'glasses_selection_tab_bar_view.dart';
export 'hats_selection_tab_bar_view.dart';
export 'miscellaneous_selection_tab_bar_view.dart';
export 'primary_selection_tab_view.dart';
export 'props_grid_view.dart';
export 'props_selection_element.dart';
export 'props_selection_tab_bar_view.dart';
export 'selection_layer.dart';

View File

@ -2,7 +2,6 @@ import 'package:avatar_detector_repository/avatar_detector_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/avatar_detector/avatar_detector.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:io_photobooth/share/share.dart';

View File

@ -13,8 +13,14 @@ class PhotoboothCharacter extends StatelessWidget {
(AvatarDetectorBloc bloc) => bloc.state.avatar,
);
final hatSelected = context
.select((InExperienceSelectionBloc bloc) => bloc.state.selectedHat);
final hat =
context.select((InExperienceSelectionBloc bloc) => bloc.state.hat);
final glasses =
context.select((InExperienceSelectionBloc bloc) => bloc.state.glasses);
final clothes =
context.select((InExperienceSelectionBloc bloc) => bloc.state.clothes);
final handheldlLeft = context
.select((InExperienceSelectionBloc bloc) => bloc.state.handheldlLeft);
// TODO(alestiago): Check out if normalizations is sufficient for all
// characters and devices.
@ -31,7 +37,10 @@ class PhotoboothCharacter extends StatelessWidget {
duration: const Duration(milliseconds: 200),
child: DashAnimation(
avatar: avatar,
hatSelected: hatSelected,
hat: hat,
glasses: glasses,
clothes: clothes,
handheldlLeft: handheldlLeft,
),
);
}

View File

@ -50,7 +50,7 @@ class BackgroundAnimationState extends State<BackgroundAnimation>
void _onRiveInit(Artboard artboard) {
backgroundController = BackgroundAnimationStateMachineController(artboard);
backgroundController!.background
.change(widget.backgroundSelected.toDouble());
.change(widget.backgroundSelected.riveIndex);
artboard.addController(backgroundController!);
_animationController.addListener(_controlBackground);
}
@ -92,7 +92,7 @@ class BackgroundAnimationState extends State<BackgroundAnimation>
final oldBackground = oldWidget.backgroundSelected;
if (newBackground != oldBackground) {
backgroundController.background
.change(widget.backgroundSelected.toDouble());
.change(widget.backgroundSelected.riveIndex);
}
}
}

View File

@ -8,11 +8,17 @@ class DashAnimation extends StatefulWidget {
const DashAnimation({
super.key,
required this.avatar,
required this.hatSelected,
required this.hat,
required this.glasses,
required this.clothes,
required this.handheldlLeft,
});
final Avatar avatar;
final Hats hatSelected;
final Hats hat;
final Glasses glasses;
final Clothes clothes;
final HandheldlLeft handheldlLeft;
@override
State<DashAnimation> createState() => DashAnimationState();
@ -69,28 +75,39 @@ class DashAnimationState extends State<DashAnimation>
..end = newOffset;
_animationController.forward(from: 0);
}
if (oldWidget.avatar.mouthDistance != widget.avatar.mouthDistance) {
dashController.mouthDistance.change(
widget.avatar.mouthDistance * 100,
);
}
if (oldWidget.avatar.rightEyeIsClosed != widget.avatar.rightEyeIsClosed) {
dashController.rightEyeIsClosed.change(
widget.avatar.rightEyeIsClosed ? 99 : 0,
);
}
if (oldWidget.avatar.leftEyeIsClosed != widget.avatar.leftEyeIsClosed) {
dashController.leftEyeIsClosed.change(
widget.avatar.leftEyeIsClosed ? 99 : 0,
);
}
if (oldWidget.hatSelected != widget.hatSelected) {
dashController.hatSelected.change(
widget.hatSelected.index.toDouble(),
if (oldWidget.hat != widget.hat) {
dashController.hats.change(
widget.hat.index.toDouble(),
);
}
if (oldWidget.glasses != widget.glasses) {
dashController.glasses.change(
widget.glasses.riveIndex,
);
}
if (oldWidget.clothes != widget.clothes) {
dashController.clothes.change(
widget.clothes.index.toDouble(),
);
}
if (oldWidget.handheldlLeft != widget.handheldlLeft) {
dashController.handheldlLeft.change(
widget.handheldlLeft.index.toDouble(),
);
}
}
@ -162,12 +179,36 @@ class DashStateMachineController extends StateMachineController {
}
const hatsInputName = 'Hats';
final hatSelected = findInput<double>(hatsInputName);
if (hatSelected is SMINumber) {
this.hatSelected = hatSelected;
final hats = findInput<double>(hatsInputName);
if (hats is SMINumber) {
this.hats = hats;
} else {
throw StateError('Could not find input "$hatsInputName"');
}
const glassesInputName = 'Glasses';
final glasses = findInput<double>(glassesInputName);
if (glasses is SMINumber) {
this.glasses = glasses;
} else {
throw StateError('Could not find input "$glassesInputName"');
}
const clothesInputName = 'Clothes';
final clothes = findInput<double>(clothesInputName);
if (clothes is SMINumber) {
this.clothes = clothes;
} else {
throw StateError('Could not find input "$clothesInputName"');
}
const handheldLeftInputName = 'HandheldLeft';
final handheldlLeft = findInput<double>(handheldLeftInputName);
if (handheldlLeft is SMINumber) {
this.handheldlLeft = handheldlLeft;
} else {
throw StateError('Could not find input "$handheldLeftInputName"');
}
}
late final SMINumber x;
@ -175,6 +216,9 @@ class DashStateMachineController extends StateMachineController {
late final SMINumber mouthDistance;
late final SMINumber leftEyeIsClosed;
late final SMINumber rightEyeIsClosed;
late final SMINumber hatSelected;
late final SMINumber hats;
late final SMINumber glasses;
late final SMINumber clothes;
late final SMINumber handheldlLeft;
}
// coverage:ignore-end

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:io_photobooth/share/share.dart';

View File

@ -1,6 +1,7 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
void main() {
group('CharacterSelectionBloc', () {

View File

@ -1,5 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
void main() {
group('CharacterSelectionSelected', () {

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:mocktail/mocktail.dart';
import '../../helpers/helpers.dart';

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:mocktail/mocktail.dart';

View File

@ -1,6 +1,5 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
void main() {
@ -12,23 +11,23 @@ void main() {
);
});
group('InExperienceSelectionHatSelected', () {
group('InExperienceSelectionHatToggled', () {
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with hat selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
act: (bloc) => bloc.add(InExperienceSelectionHatSelected(Hats.helmet)),
act: (bloc) => bloc.add(InExperienceSelectionHatToggled(Hats.helmet)),
expect: () => const <InExperienceSelectionState>[
InExperienceSelectionState(selectedHat: Hats.helmet)
InExperienceSelectionState(hat: Hats.helmet)
],
);
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with hat unselected.',
'emits state without hat when hat was previously selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
seed: () => InExperienceSelectionState(selectedHat: Hats.helmet),
act: (bloc) => bloc.add(InExperienceSelectionHatSelected(Hats.helmet)),
seed: () => InExperienceSelectionState(hat: Hats.helmet),
act: (bloc) => bloc.add(InExperienceSelectionHatToggled(Hats.helmet)),
expect: () =>
const <InExperienceSelectionState>[InExperienceSelectionState()],
);
@ -79,5 +78,88 @@ void main() {
expect: () => const <InExperienceSelectionState>[],
);
});
group('InExperienceSelectionGlassesToggled', () {
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with glasses selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
act: (bloc) =>
bloc.add(InExperienceSelectionGlassesToggled(Glasses.glasses1)),
expect: () => const <InExperienceSelectionState>[
InExperienceSelectionState(glasses: Glasses.glasses1)
],
);
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state without glasses when glasses were previously selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
seed: () => InExperienceSelectionState(glasses: Glasses.glasses1),
act: (bloc) =>
bloc.add(InExperienceSelectionGlassesToggled(Glasses.glasses1)),
expect: () =>
const <InExperienceSelectionState>[InExperienceSelectionState()],
);
});
group('InExperienceSelectionClothesToggled', () {
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with clothes selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
act: (bloc) =>
bloc.add(InExperienceSelectionClothesToggled(Clothes.clothes1)),
expect: () => const <InExperienceSelectionState>[
InExperienceSelectionState(clothes: Clothes.clothes1)
],
);
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state without clothes when clothes was previously selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
seed: () => InExperienceSelectionState(clothes: Clothes.clothes1),
act: (bloc) =>
bloc.add(InExperienceSelectionClothesToggled(Clothes.clothes1)),
expect: () =>
const <InExperienceSelectionState>[InExperienceSelectionState()],
);
});
group('InExperienceSelectionHandleheldLeftToggled', () {
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with handleheld left selected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
act: (bloc) => bloc.add(
InExperienceSelectionHandleheldLeftToggled(
HandheldlLeft.handheldLeft1,
),
),
expect: () => const <InExperienceSelectionState>[
InExperienceSelectionState(
handheldlLeft: HandheldlLeft.handheldLeft1,
)
],
);
blocTest<InExperienceSelectionBloc, InExperienceSelectionState>(
'emits state with handleheld left unselected.',
build: () =>
InExperienceSelectionBloc(characterPreSelected: Character.dash),
seed: () => InExperienceSelectionState(
handheldlLeft: HandheldlLeft.handheldLeft1,
),
act: (bloc) => bloc.add(
InExperienceSelectionHandleheldLeftToggled(
HandheldlLeft.handheldLeft1,
),
),
expect: () => const <InExperienceSelectionState>[
InExperienceSelectionState(),
],
);
});
});
}

View File

@ -3,11 +3,11 @@ import 'package:io_photobooth/in_experience_selection/in_experience_selection.da
void main() {
group('InExperienceSelectionEvent', () {
group('InExperienceSelectionHatSelected', () {
group('InExperienceSelectionHatToggled', () {
test('support value comparison', () {
final eventA = InExperienceSelectionHatSelected(Hats.helmet);
final eventB = InExperienceSelectionHatSelected(Hats.helmet);
final eventC = InExperienceSelectionHatSelected(Hats.none);
final eventA = InExperienceSelectionHatToggled(Hats.helmet);
final eventB = InExperienceSelectionHatToggled(Hats.helmet);
final eventC = InExperienceSelectionHatToggled(Hats.none);
expect(eventA, equals(eventB));
expect(eventA, isNot(equals(eventC)));
});

View File

@ -2,7 +2,6 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:mocktail/mocktail.dart';

View File

@ -0,0 +1,169 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:mocktail/mocktail.dart';
import '../../helpers/helpers.dart';
class _MockInExperienceSelectionBloc
extends MockBloc<InExperienceSelectionEvent, InExperienceSelectionState>
implements InExperienceSelectionBloc {}
void main() {
group('PropsSelectionTabBarView', () {
late InExperienceSelectionBloc inExperienceSelectionBloc;
setUp(() {
inExperienceSelectionBloc = _MockInExperienceSelectionBloc();
when(() => inExperienceSelectionBloc.state)
.thenReturn(InExperienceSelectionState());
});
testWidgets(
'display HatsSelectionTabBarView by default',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(),
inExperienceSelectionBloc,
);
expect(find.byType(HatsSelectionTabBarView), findsOneWidget);
},
);
testWidgets(
'display GlassesSelectionTabBarView by tapping on glasses tab',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(),
inExperienceSelectionBloc,
);
final finder = find.byKey(PropsSelectionTabBarView.glassesTabKey);
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pumpAndSettle();
expect(find.byType(GlassesSelectionTabBarView), findsOneWidget);
},
);
testWidgets(
'display ClothesSelectionTabBarView by tapping on clothes tab',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(),
inExperienceSelectionBloc,
);
final finder = find.byKey(PropsSelectionTabBarView.clothesTabKey);
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pumpAndSettle();
expect(find.byType(ClothesSelectionTabBarView), findsOneWidget);
},
);
testWidgets(
'display OthersSelectionTabBarView by tapping on others tab',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(),
inExperienceSelectionBloc,
);
final finder = find.byKey(PropsSelectionTabBarView.othersTabKey);
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pumpAndSettle();
expect(find.byType(MiscellaneousSelectionTabBarView), findsOneWidget);
},
);
testWidgets(
'adds InExperienceSelectionHatToggled tapping on a hat',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(),
inExperienceSelectionBloc,
);
const hat = Hats.helmet;
await tester
.tap(find.byKey(HatsSelectionTabBarView.hatSelectionKey(hat)));
verify(
() => inExperienceSelectionBloc
.add(InExperienceSelectionHatToggled(hat)),
).called(1);
},
);
testWidgets(
'adds InExperienceSelectionGlassesToggled tapping on a glasses',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(initialIndex: 1),
inExperienceSelectionBloc,
);
const glasses = Glasses.glasses1;
await tester.tap(
find.byKey(GlassesSelectionTabBarView.glassesSelectionKey(glasses)),
);
verify(
() => inExperienceSelectionBloc
.add(InExperienceSelectionGlassesToggled(glasses)),
).called(1);
},
);
testWidgets(
'adds InExperienceSelectionClothesToggled tapping on a clothes',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(initialIndex: 2),
inExperienceSelectionBloc,
);
const clothes = Clothes.clothes1;
await tester.tap(
find.byKey(ClothesSelectionTabBarView.clothesSelectionKey(clothes)),
);
verify(
() => inExperienceSelectionBloc
.add(InExperienceSelectionClothesToggled(clothes)),
).called(1);
},
);
testWidgets(
'adds InExperienceSelectionHandleheldLeftToggled tapping on '
'handheld left',
(WidgetTester tester) async {
await tester.pumpSubject(
PropsSelectionTabBarView(initialIndex: 3),
inExperienceSelectionBloc,
);
const item = HandheldlLeft.handheldLeft1;
await tester.tap(
find.byKey(
MiscellaneousSelectionTabBarView.miscellaneousSelectionKey(item),
),
);
verify(
() => inExperienceSelectionBloc
.add(InExperienceSelectionHandleheldLeftToggled(item)),
).called(1);
},
);
});
}
extension on WidgetTester {
Future<void> pumpSubject(
PropsSelectionTabBarView subject,
InExperienceSelectionBloc inExperienceSelectionBloc,
) =>
pumpApp(
MultiBlocProvider(
providers: [BlocProvider.value(value: inExperienceSelectionBloc)],
child: Scaffold(
body: subject,
),
),
);
}

View File

@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/avatar_detector/avatar_detector.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/in_experience_selection/in_experience_selection.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:io_photobooth/share/share.dart';

View File

@ -16,7 +16,10 @@ void main() {
rightEyeIsClosed: false,
distance: 0.5,
);
var hatSelected = Hats.none;
var selectedHat = Hats.none;
var selectedGlasses = Glasses.none;
var selectedClothes = Clothes.none;
var selectedHandheldlLeft = HandheldlLeft.none;
late StateSetter stateSetter;
await tester.pumpWidget(
@ -26,7 +29,10 @@ void main() {
stateSetter = setState;
return DashAnimation(
avatar: avatar,
hatSelected: hatSelected,
hat: selectedHat,
glasses: selectedGlasses,
clothes: selectedClothes,
handheldlLeft: selectedHandheldlLeft,
);
},
),
@ -37,9 +43,12 @@ void main() {
final state =
tester.state(find.byType(DashAnimation)) as DashAnimationState;
final controller = state.dashController;
final x = controller?.x.value;
final y = controller?.y.value;
final hatSelectedValue = controller?.hatSelected.value;
final xValue = controller?.x.value;
final yValue = controller?.y.value;
final hatsValue = controller?.hats.value;
final glassesValue = controller?.glasses.value;
final clothesValue = controller?.clothes.value;
final handheldlLeftValue = controller?.handheldlLeft.value;
stateSetter(() {
avatar = Avatar(
@ -50,7 +59,10 @@ void main() {
rightEyeIsClosed: !avatar.rightEyeIsClosed,
distance: avatar.distance,
);
hatSelected = Hats.helmet;
selectedHat = Hats.helmet;
selectedGlasses = Glasses.glasses1;
selectedClothes = Clothes.clothes1;
selectedHandheldlLeft = HandheldlLeft.handheldLeft1;
});
await tester.pump(Duration(milliseconds: 150));
await tester.pump(Duration(milliseconds: 150));
@ -58,9 +70,15 @@ void main() {
expect(controller?.mouthDistance.value, avatar.mouthDistance * 100);
expect(controller?.leftEyeIsClosed.value, 99);
expect(controller?.rightEyeIsClosed.value, 99);
expect(controller?.x.value, isNot(equals(x)));
expect(controller?.y.value, isNot(equals(y)));
expect(controller?.hatSelected.value, isNot(hatSelectedValue));
expect(controller?.x.value, isNot(equals(xValue)));
expect(controller?.y.value, isNot(equals(yValue)));
expect(controller?.hats.value, isNot(hatsValue));
expect(controller?.glasses.value, isNot(glassesValue));
expect(controller?.clothes.value, isNot(clothesValue));
expect(
controller?.handheldlLeft.value,
isNot(handheldlLeftValue),
);
await tester.pump(kThemeAnimationDuration);
});
});