feat: use composited image in share modal (#566)

This commit is contained in:
Felix Angelov
2021-05-11 13:22:43 -05:00
committed by GitHub
parent dfc09e454c
commit 4e66a4d738
16 changed files with 89 additions and 200 deletions

2
.vscode/launch.json vendored
View File

@ -9,7 +9,7 @@
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"args": ["-d", "chrome"]
"args": ["-d", "chrome", "--web-renderer", "html"]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,68 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:io_photobooth/photobooth/photobooth.dart';
const _mobileMargin = EdgeInsets.all(15);
const _mobilePadding = EdgeInsets.only(
bottom: 30,
left: 19,
right: 10,
top: 10,
);
const _desktopMargin = EdgeInsets.all(20);
const _desktopPadding = EdgeInsets.only(
bottom: 30,
left: 39,
right: 19,
top: 5,
);
/// A widget that displays [CharactersLayer] and [StickersLayer] on top of
/// the raw [image] took from the camera.
///
/// The [FramedPhotoboothPhoto] widget is styled to mimic a framed card photo.
class FramedPhotoboothPhoto extends StatelessWidget {
const FramedPhotoboothPhoto({
Key? key,
required this.image,
required this.aspectRatio,
this.isTilted = true,
}) : super(key: key);
final double aspectRatio;
final String image;
final bool isTilted;
@override
Widget build(BuildContext context) {
final isMobile = aspectRatio < 1;
var photo = Center(
child: AspectRatio(
aspectRatio: aspectRatio,
child: Container(
margin: isMobile ? _mobileMargin : _desktopMargin,
padding: isMobile ? _mobilePadding : _desktopPadding,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(
isMobile
? 'assets/images/photo_frame_mobile.png'
: 'assets/images/photo_frame.png',
),
),
),
child: PhotoboothPhoto(image: image),
),
),
);
if (!isTilted) return photo;
return Transform(
alignment: const Alignment(0, -3 / 4),
transform: Matrix4.identity()..rotateZ(-5 * (math.pi / 180)),
child: photo,
);
}
}

View File

@ -2,7 +2,6 @@ export 'animated_characters/animated_characters.dart';
export 'character_icon_button.dart';
export 'characters_caption.dart';
export 'characters_layer.dart';
export 'framed_photobooth_photo.dart';
export 'photobooth_background.dart';
export 'photobooth_error.dart';
export 'photobooth_photo.dart';

View File

@ -1,7 +1,7 @@
import 'package:camera/camera.dart';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:io_photobooth/photobooth/photobooth.dart';
import 'package:io_photobooth/share/share.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
@ -11,7 +11,7 @@ class ShareBottomSheet extends StatelessWidget {
required this.image,
}) : super(key: key);
final CameraImage image;
final Uint8List image;
@override
Widget build(BuildContext context) {
@ -36,14 +36,8 @@ class ShareBottomSheet extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: FramedPhotoboothPhoto(
aspectRatio: PhotoboothAspectRatio.portrait,
image: image.data,
),
),
const SizedBox(height: 60),
SharePreviewPhoto(image: image),
const SizedBox(height: 60),
SelectableText(
l10n.shareDialogHeading,

View File

@ -1,19 +1,14 @@
import 'package:camera/camera.dart';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:io_photobooth/photobooth/photobooth.dart';
import 'package:io_photobooth/share/share.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class ShareDialog extends StatelessWidget {
const ShareDialog({
Key? key,
required this.aspectRatio,
required this.image,
}) : super(key: key);
const ShareDialog({Key? key, required this.image}) : super(key: key);
final double aspectRatio;
final CameraImage image;
final Uint8List image;
@override
Widget build(BuildContext context) {
@ -44,14 +39,7 @@ class ShareDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
height: 430,
width: 600,
child: FramedPhotoboothPhoto(
aspectRatio: aspectRatio,
image: image.data,
),
),
SharePreviewPhoto(image: image),
const SizedBox(height: 60),
SelectableText(
l10n.shareDialogHeading,

View File

@ -1,4 +1,5 @@
import 'package:camera/camera.dart';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -18,6 +19,9 @@ class ShareBody extends StatelessWidget {
final compositeStatus = context.select(
(ShareBloc bloc) => bloc.state.compositeStatus,
);
final compositedImage = context.select(
(ShareBloc bloc) => bloc.state.bytes,
);
final isUploadSuccess = context.select(
(ShareBloc bloc) => bloc.state.uploadStatus.isSuccess,
);
@ -55,12 +59,14 @@ class ShareBody extends StatelessWidget {
),
child: ShareCopyableLink(link: shareUrl),
),
if (image != null && file != null)
if (compositedImage != null && file != null)
ResponsiveLayoutBuilder(
small: (_, __) =>
MobileButtonsLayout(image: image, file: file),
small: (_, __) => MobileButtonsLayout(
image: compositedImage,
file: file,
),
large: (_, __) => DesktopButtonsLayout(
image: image,
image: compositedImage,
file: file,
),
),
@ -87,7 +93,7 @@ class DesktopButtonsLayout extends StatelessWidget {
required this.file,
}) : super(key: key);
final CameraImage image;
final Uint8List image;
final XFile file;
@override
@ -113,7 +119,7 @@ class MobileButtonsLayout extends StatelessWidget {
required this.file,
}) : super(key: key);
final CameraImage image;
final Uint8List image;
final XFile file;
@override

View File

@ -1,4 +1,5 @@
import 'package:camera/camera.dart';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_photobooth/l10n/l10n.dart';
@ -16,8 +17,8 @@ class ShareButton extends StatelessWidget {
}) : platformHelper = platformHelper ?? PlatformHelper(),
super(key: key);
/// Raw image from camera
final CameraImage image;
/// Composited image
final Uint8List image;
/// Optional [PlatformHelper] instance.
final PlatformHelper platformHelper;
@ -36,10 +37,7 @@ class ShareButton extends StatelessWidget {
BlocProvider.value(value: context.read<PhotoboothBloc>()),
BlocProvider.value(value: context.read<ShareBloc>()),
],
child: ShareDialog(
aspectRatio: PhotoboothAspectRatio.landscape,
image: image,
),
child: ShareDialog(image: image),
),
portraitChild: MultiBlocProvider(
providers: [

View File

@ -0,0 +1,36 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class SharePreviewPhoto extends StatelessWidget {
const SharePreviewPhoto({Key? key, required this.image}) : super(key: key);
final Uint8List image;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: -5 * (math.pi / 180),
child: Container(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 400),
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: PhotoboothColors.black54,
blurRadius: 7,
offset: Offset(-3, 9),
spreadRadius: 1,
),
],
),
child: Image.memory(
image,
isAntiAlias: true,
filterQuality: FilterQuality.high,
),
),
);
}
}

View File

@ -7,6 +7,7 @@ export 'share_button.dart';
export 'share_caption.dart';
export 'share_copyable_link.dart';
export 'share_heading.dart';
export 'share_preview_photo.dart';
export 'share_progress_overlay.dart';
export 'share_social_media_clarification.dart';
export 'share_state_listener.dart';

View File

@ -1,66 +0,0 @@
// ignore_for_file: prefer_const_constructors
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/photobooth/photobooth.dart';
import 'package:mocktail/mocktail.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
import '../../helpers/helpers.dart';
class FakePhotoboothEvent extends Fake implements PhotoboothEvent {}
class FakePhotoboothState extends Fake implements PhotoboothState {}
void main() {
const width = 1;
const height = 1;
const data = '';
const image = CameraImage(width: width, height: height, data: data);
const aspectRatio = PhotoboothAspectRatio.landscape;
late PhotoboothBloc photoboothBloc;
setUpAll(() {
registerFallbackValue<PhotoboothEvent>(FakePhotoboothEvent());
registerFallbackValue<PhotoboothState>(FakePhotoboothState());
});
setUp(() {
photoboothBloc = MockPhotoboothBloc();
when(() => photoboothBloc.state).thenReturn(PhotoboothState(image: image));
});
group('FramedPhotoboothPhoto', () {
testWidgets('displays PhotoboothPhoto', (tester) async {
await tester.pumpApp(
FramedPhotoboothPhoto(aspectRatio: aspectRatio, image: data),
photoboothBloc: photoboothBloc,
);
expect(find.byType(PhotoboothPhoto), findsOneWidget);
});
testWidgets('transform image by default', (tester) async {
await tester.pumpApp(
FramedPhotoboothPhoto(aspectRatio: aspectRatio, image: data),
photoboothBloc: photoboothBloc,
);
expect(find.byType(Transform), findsOneWidget);
});
testWidgets(
'does not transform image '
'when isTilted is false', (tester) async {
await tester.pumpApp(
FramedPhotoboothPhoto(
aspectRatio: aspectRatio,
image: data,
isTilted: false,
),
photoboothBloc: photoboothBloc,
);
expect(find.byType(Transform), findsNothing);
});
});
}

View File

@ -1,4 +1,6 @@
// ignore_for_file: prefer_const_constructors
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@ -17,6 +19,7 @@ void main() {
const height = 1;
const data = '';
const image = CameraImage(width: width, height: height, data: data);
final bytes = Uint8List.fromList(transparentImage);
late PhotoboothBloc photoboothBloc;
@ -33,7 +36,7 @@ void main() {
group('ShareBottomSheet', () {
testWidgets('displays heading', (tester) async {
await tester.pumpApp(
Scaffold(body: ShareBottomSheet(image: image)),
Scaffold(body: ShareBottomSheet(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byKey(Key('shareBottomSheet_heading')), findsOneWidget);
@ -41,7 +44,7 @@ void main() {
testWidgets('displays subheading', (tester) async {
await tester.pumpApp(
Scaffold(body: ShareBottomSheet(image: image)),
Scaffold(body: ShareBottomSheet(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byKey(Key('shareBottomSheet_subheading')), findsOneWidget);
@ -49,7 +52,7 @@ void main() {
testWidgets('displays a TwitterButton', (tester) async {
await tester.pumpApp(
Scaffold(body: ShareBottomSheet(image: image)),
Scaffold(body: ShareBottomSheet(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byType(TwitterButton), findsOneWidget);
@ -57,7 +60,7 @@ void main() {
testWidgets('displays a FacebookButton', (tester) async {
await tester.pumpApp(
Scaffold(body: ShareBottomSheet(image: image)),
Scaffold(body: ShareBottomSheet(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byType(FacebookButton), findsOneWidget);
@ -65,7 +68,7 @@ void main() {
testWidgets('taps on close will dismiss the popup', (tester) async {
await tester.pumpApp(
Scaffold(body: ShareBottomSheet(image: image)),
Scaffold(body: ShareBottomSheet(image: bytes)),
photoboothBloc: photoboothBloc,
);
await tester.tap(find.byIcon(Icons.clear));

View File

@ -1,11 +1,12 @@
// ignore_for_file: prefer_const_constructors
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/photobooth/photobooth.dart';
import 'package:io_photobooth/share/share.dart';
import 'package:mocktail/mocktail.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
import '../../helpers/helpers.dart';
@ -18,7 +19,7 @@ void main() {
const height = 1;
const data = '';
const image = CameraImage(width: width, height: height, data: data);
const aspectRatio = PhotoboothAspectRatio.landscape;
final bytes = Uint8List.fromList(transparentImage);
late PhotoboothBloc photoboothBloc;
@ -35,7 +36,7 @@ void main() {
group('ShareDialog', () {
testWidgets('displays heading', (tester) async {
await tester.pumpApp(
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
Material(child: ShareDialog(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byKey(Key('shareDialog_heading')), findsOneWidget);
@ -43,7 +44,7 @@ void main() {
testWidgets('displays subheading', (tester) async {
await tester.pumpApp(
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
Material(child: ShareDialog(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byKey(Key('shareDialog_subheading')), findsOneWidget);
@ -51,7 +52,7 @@ void main() {
testWidgets('displays a TwitterButton', (tester) async {
await tester.pumpApp(
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
Material(child: ShareDialog(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byType(TwitterButton), findsOneWidget);
@ -59,7 +60,7 @@ void main() {
testWidgets('displays a FacebookButton', (tester) async {
await tester.pumpApp(
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
Material(child: ShareDialog(image: bytes)),
photoboothBloc: photoboothBloc,
);
expect(find.byType(FacebookButton), findsOneWidget);
@ -67,7 +68,7 @@ void main() {
testWidgets('taps on close will dismiss the popup', (tester) async {
await tester.pumpApp(
Material(child: ShareDialog(aspectRatio: aspectRatio, image: image)),
Material(child: ShareDialog(image: bytes)),
photoboothBloc: photoboothBloc,
);
await tester.tap(find.byIcon(Icons.clear));

View File

@ -146,6 +146,7 @@ void main() {
setUp(() {
when(() => shareBloc.state).thenReturn(ShareState(
compositeStatus: ShareStatus.success,
bytes: Uint8List(0),
file: file,
));
});
@ -328,10 +329,6 @@ void main() {
headers: const {},
),
).thenAnswer((_) async => true);
when(() => shareBloc.state).thenReturn(ShareState(
compositeStatus: ShareStatus.success,
file: file,
));
tester.setDisplaySize(Size(2500, 2500));
await tester.pumpApp(
ShareView(),

View File

@ -1,4 +1,6 @@
// ignore_for_file: prefer_const_constructors
import 'dart:typed_data';
import 'package:bloc_test/bloc_test.dart';
import 'package:camera/camera.dart';
import 'package:flutter_test/flutter_test.dart';
@ -23,6 +25,7 @@ void main() {
const height = 1;
const data = '';
const image = CameraImage(width: width, height: height, data: data);
final bytes = Uint8List.fromList(transparentImage);
late PhotoboothBloc photoboothBloc;
late PlatformHelper platformHelper;
@ -45,10 +48,7 @@ void main() {
when(() => platformHelper.isMobile).thenReturn(true);
await tester.pumpApp(
ShareButton(
image: image,
platformHelper: platformHelper,
),
ShareButton(image: bytes),
photoboothBloc: photoboothBloc,
);
@ -66,7 +66,7 @@ void main() {
await tester.pumpApp(
ShareButton(
image: image,
image: bytes,
platformHelper: platformHelper,
),
photoboothBloc: photoboothBloc,
@ -85,7 +85,7 @@ void main() {
tester.setLandscapeDisplaySize();
await tester.pumpApp(
ShareButton(
image: image,
image: bytes,
platformHelper: platformHelper,
),
photoboothBloc: photoboothBloc,