Skip to content
This repository was archived by the owner on Aug 14, 2023. It is now read-only.

feat: add camera selection dropdown to the animoji intro page #435

Merged
merged 11 commits into from
Feb 27, 2023
2 changes: 0 additions & 2 deletions lib/animoji_intro/view/animoji_intro_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:holobooth/animoji_intro/animoji_intro.dart';
import 'package:holobooth/camera/camera.dart';
import 'package:holobooth/footer/footer.dart';
import 'package:holobooth_ui/holobooth_ui.dart';

Expand All @@ -26,7 +25,6 @@ class AnimojiIntroView extends StatelessWidget {
Widget build(BuildContext context) {
return Stack(
children: [
Align(child: CameraView(onCameraReady: (_) => {})),
const Positioned.fill(child: AnimojiIntroBackground()),
Positioned.fill(
child: Column(
Expand Down
65 changes: 35 additions & 30 deletions lib/animoji_intro/widgets/animoji_intro_body.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/animoji_intro/animoji_intro.dart';
import 'package:holobooth/camera/camera.dart';
import 'package:holobooth/l10n/l10n.dart';
import 'package:holobooth_ui/holobooth_ui.dart';

Expand Down Expand Up @@ -85,40 +87,43 @@ class _BottomContent extends StatelessWidget {
final l10n = context.l10n;
final textTheme = Theme.of(context).textTheme;

return Container(
color: HoloBoothColors.modalSurface,
padding: const EdgeInsets.all(20),
child: Flex(
direction: smallScreen ? Axis.vertical : Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(
child: ShaderMask(
shaderCallback: (bounds) {
return HoloBoothGradients.secondaryFive
.createShader(Offset.zero & bounds.size);
},
child: const Icon(
Icons.videocam_rounded,
size: 40,
color: HoloBoothColors.white,
return BlocProvider(
create: (context) => CameraBloc()..add(CameraStarted()),
child: Container(
color: HoloBoothColors.modalSurface,
padding: const EdgeInsets.all(20),
child: Flex(
direction: smallScreen ? Axis.vertical : Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(
child: ShaderMask(
shaderCallback: (bounds) {
return HoloBoothGradients.secondaryFive
.createShader(Offset.zero & bounds.size);
},
child: const Icon(
Icons.videocam_rounded,
size: 40,
color: HoloBoothColors.white,
),
),
),
),
Flexible(
flex: smallScreen ? 0 : 3,
child: SelectableText(
l10n.animojiIntroPageSubheading,
key: const Key('animojiIntro_subheading_text'),
style: textTheme.bodyLarge?.copyWith(
color: HoloBoothColors.white,
Flexible(
flex: smallScreen ? 0 : 3,
child: SelectableText(
l10n.animojiIntroPageSubheading,
key: const Key('animojiIntro_subheading_text'),
style: textTheme.bodyLarge?.copyWith(
color: HoloBoothColors.white,
),
textAlign: smallScreen ? TextAlign.center : TextAlign.left,
),
textAlign: smallScreen ? TextAlign.center : TextAlign.left,
),
),
if (smallScreen) const SizedBox(height: 16),
const Flexible(child: AnimojiNextButton()),
],
const CameraSelectionDropdown(),
const Flexible(child: AnimojiNextButton()),
],
),
),
);
}
Expand Down
5 changes: 4 additions & 1 deletion lib/animoji_intro/widgets/animoji_next_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/assets/assets.dart';
import 'package:holobooth/audio_player/audio_player.dart';
import 'package:holobooth/camera/bloc/camera_bloc.dart';
import 'package:holobooth/l10n/l10n.dart';
import 'package:holobooth/photo_booth/photo_booth.dart';
import 'package:holobooth_ui/holobooth_ui.dart';
Expand Down Expand Up @@ -32,7 +33,9 @@ class _AnimojiNextButtonState extends State<AnimojiNextButton> {
label: 'start-holobooth',
),
);
Navigator.of(context).push(PhotoBoothPage.route());
Navigator.of(context).push(
PhotoBoothPage.route(context.read<CameraBloc>().state.camera),
);
},
child: Text(l10n.nextButtonText),
),
Expand Down
44 changes: 44 additions & 0 deletions lib/camera/bloc/camera_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:camera/camera.dart';
import 'package:equatable/equatable.dart';

part 'camera_event.dart';

part 'camera_state.dart';

class CameraBloc extends Bloc<CameraEvent, CameraState> {
CameraBloc() : super(const CameraState()) {
on<CameraStarted>(_onCameraStarted);
on<CameraChanged>(_onCameraChangedEvent);
}

FutureOr<void> _onCameraStarted(
CameraStarted event,
Emitter<CameraState> emit,
) async {
if (state.availableCameras != null) {
return;
}

try {
final cameras = await availableCameras();
emit(
CameraState(
availableCameras: cameras,
camera: cameras.isNotEmpty ? cameras[0] : null,
),
);
} catch (error) {
emit(CameraState(cameraError: error));
}
}

FutureOr<void> _onCameraChangedEvent(
CameraChanged event,
Emitter<CameraState> emit,
) {
emit(state.copyWith(camera: event.camera));
}
}
19 changes: 19 additions & 0 deletions lib/camera/bloc/camera_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
part of 'camera_bloc.dart';

abstract class CameraEvent extends Equatable {
const CameraEvent();
}

class CameraStarted extends CameraEvent {
@override
List<Object?> get props => [];
}

class CameraChanged extends CameraEvent {
const CameraChanged(this.camera);

final CameraDescription camera;

@override
List<Object?> get props => [camera];
}
28 changes: 28 additions & 0 deletions lib/camera/bloc/camera_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
part of 'camera_bloc.dart';

class CameraState extends Equatable {
const CameraState({
this.camera,
this.availableCameras,
this.cameraError,
});

final CameraDescription? camera;
final List<CameraDescription>? availableCameras;
final Object? cameraError;

CameraState copyWith({
CameraDescription? camera,
List<CameraDescription>? availableCameras,
Object? cameraError,
}) {
return CameraState(
camera: camera ?? this.camera,
availableCameras: availableCameras ?? this.availableCameras,
cameraError: cameraError ?? this.cameraError,
);
}

@override
List<Object?> get props => [camera, availableCameras, cameraError];
}
1 change: 1 addition & 0 deletions lib/camera/camera.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'bloc/camera_bloc.dart';
export 'widgets/widgets.dart';
116 changes: 116 additions & 0 deletions lib/camera/widgets/camera_selection_dropdown.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:holobooth/camera/camera.dart';
import 'package:holobooth/l10n/l10n.dart';
import 'package:holobooth_ui/holobooth_ui.dart';

class CameraSelectionDropdown extends StatelessWidget {
const CameraSelectionDropdown({super.key});

@visibleForTesting
static const cameraErrorViewKey = Key('camera_error_view');

@override
Widget build(BuildContext context) {
return BlocBuilder<CameraBloc, CameraState>(
buildWhen: (previous, current) =>
previous.availableCameras != current.availableCameras ||
previous.cameraError != current.cameraError,
builder: (_, state) {
if (state.cameraError != null) {
return _CameraErrorView(
key: cameraErrorViewKey,
error: state.cameraError!,
);
}

if (state.availableCameras == null) {
return const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(
color: HoloBoothColors.convertLoading,
),
);
}

final cameras = state.availableCameras;
if (cameras == null || cameras.isEmpty) {
return _CameraErrorView(
key: cameraErrorViewKey,
error: CameraException('cameraNotFound', 'Camera not found'),
);
}

final dropdownItems = cameras
.map(
(camera) => DropdownMenuItem(
value: camera,
child: Text(
camera.name,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(color: HoloBoothColors.white),
),
),
)
.toList();

return BlocBuilder<CameraBloc, CameraState>(
builder: (context, state) => DropdownButton<CameraDescription>(
borderRadius: const BorderRadius.all(Radius.circular(12)),
value: state.camera,
dropdownColor: HoloBoothColors.darkPurple,
items: dropdownItems,
onChanged: (value) =>
context.read<CameraBloc>().add(CameraChanged(value!)),
),
);
},
);
}
}

class _CameraErrorView extends StatelessWidget {
const _CameraErrorView({super.key, required this.error});

final Object error;

@override
Widget build(BuildContext context) {
const color = HoloBoothColors.red;
final l10n = context.l10n;

var errorMessage = l10n.unknownCameraErrorMessage;
if (error is CameraException) {
final cameraException = error as CameraException;
switch (cameraException.code) {
case 'CameraAccessDenied':
errorMessage = l10n.cameraAccessDeniedMessage;
break;
default:
errorMessage = l10n.cameraNotFoundMessage;
}
}

return Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.videocam_off,
color: color,
),
const SizedBox(width: 12),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: Text(
errorMessage,
style:
Theme.of(context).textTheme.bodySmall?.copyWith(color: color),
),
),
],
);
}
}
7 changes: 5 additions & 2 deletions lib/camera/widgets/camera_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CameraView extends StatefulWidget {
const CameraView({
super.key,
required this.onCameraReady,
this.camera,
});

@visibleForTesting
Expand All @@ -21,6 +22,8 @@ class CameraView extends StatefulWidget {

final void Function(CameraController controller)? onCameraReady;

final CameraDescription? camera;

@override
State<CameraView> createState() => _CameraViewState();
}
Expand All @@ -45,9 +48,9 @@ class _CameraViewState extends State<CameraView> {
_cameraControllerCompleter = Completer<void>();

try {
final cameras = await availableCameras();
final camera = widget.camera ?? (await availableCameras())[0];
_cameraController = CameraController(
cameras[0],
camera,
ResolutionPreset.high,
enableAudio: false,
);
Expand Down
1 change: 1 addition & 0 deletions lib/camera/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'camera_error_view.dart';
export 'camera_selection_dropdown.dart';
export 'camera_view.dart';
Loading