mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
## Summary - add inline profile photo capture with smooth avatar-to-viewfinder transitions - add close, flip, shutter, retry, and use-photo states with haptics and front-camera mirroring - polish iOS liquid-glass controls and avatar editor motion ### Related issue Follow-up to #6583. ### Testing - just mobile-check - just mobile-test (1,743 tests) - built, installed, and launched on Pixel 10 and iPhone Air --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Code Reviewer <037593536284cf40e221c96c931e9877d4166d54f6bb84e5341a86d7fd5d05a4@buzz.block.builderlab.xyz> Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
249 lines
7.2 KiB
Dart
249 lines
7.2 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:buzz/features/profile/image_avatar_capture.dart';
|
|
import 'package:buzz/shared/relay/relay.dart';
|
|
import 'package:buzz/shared/theme/theme.dart';
|
|
import 'package:camera_platform_interface/camera_platform_interface.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
void main() {
|
|
testWidgets('resumes with a replacement after native disposal fails', (
|
|
tester,
|
|
) async {
|
|
final platform = _TestCameraPlatform(blockDisposeForCameraIds: {1});
|
|
final previousPlatform = CameraPlatform.instance;
|
|
CameraPlatform.instance = platform;
|
|
addTearDown(() => CameraPlatform.instance = previousPlatform);
|
|
final lifecycle = _TestLifecycleNotifier();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [appLifecycleProvider.overrideWith(() => lifecycle)],
|
|
child: MaterialApp(
|
|
theme: AppTheme.light(),
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
height: 400,
|
|
child: ImageAvatarCapture(
|
|
height: 400,
|
|
onAccepted: (_) {},
|
|
onClosed: () {},
|
|
loadCameras: platform.availableCameras,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.createdCameraIds, [1]);
|
|
|
|
lifecycle.setLifecycle(AppLifecycleState.paused);
|
|
await tester.pump();
|
|
expect(platform.disposedCameraIds, [1]);
|
|
|
|
lifecycle.setLifecycle(AppLifecycleState.resumed);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.createdCameraIds, [1]);
|
|
|
|
platform.failDispose(1);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(platform.createdCameraIds, [1, 2]);
|
|
final shutter = tester.widget<InkWell>(
|
|
find.byKey(const ValueKey('image-camera-shutter')),
|
|
);
|
|
expect(shutter.onTap, isNotNull);
|
|
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets('waits for an in-flight camera before lifecycle replacement', (
|
|
tester,
|
|
) async {
|
|
final platform = _TestCameraPlatform(
|
|
blockDisposeForCameraIds: {1},
|
|
blockInitializeForCameraIds: {1},
|
|
);
|
|
final previousPlatform = CameraPlatform.instance;
|
|
CameraPlatform.instance = platform;
|
|
addTearDown(() => CameraPlatform.instance = previousPlatform);
|
|
final lifecycle = _TestLifecycleNotifier();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [appLifecycleProvider.overrideWith(() => lifecycle)],
|
|
child: MaterialApp(
|
|
theme: AppTheme.light(),
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
height: 400,
|
|
child: ImageAvatarCapture(
|
|
height: 400,
|
|
onAccepted: (_) {},
|
|
onClosed: () {},
|
|
loadCameras: platform.availableCameras,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.createdCameraIds, [1]);
|
|
|
|
lifecycle.setLifecycle(AppLifecycleState.paused);
|
|
await tester.pump();
|
|
lifecycle.setLifecycle(AppLifecycleState.resumed);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.createdCameraIds, [1]);
|
|
|
|
platform.completeInitialize(1);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.disposedCameraIds, [1]);
|
|
expect(platform.createdCameraIds, [1]);
|
|
|
|
platform.completeDispose(1);
|
|
await tester.pump();
|
|
await tester.pump();
|
|
expect(platform.createdCameraIds, [1, 2]);
|
|
final shutter = tester.widget<InkWell>(
|
|
find.byKey(const ValueKey('image-camera-shutter')),
|
|
);
|
|
expect(shutter.onTap, isNotNull);
|
|
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump();
|
|
});
|
|
}
|
|
|
|
class _TestLifecycleNotifier extends AppLifecycleNotifier {
|
|
AppLifecycleState _lifecycle = AppLifecycleState.resumed;
|
|
|
|
@override
|
|
AppLifecycleState build() => _lifecycle;
|
|
|
|
void setLifecycle(AppLifecycleState value) {
|
|
_lifecycle = value;
|
|
state = value;
|
|
}
|
|
}
|
|
|
|
class _TestCameraPlatform extends CameraPlatform {
|
|
_TestCameraPlatform({
|
|
Set<int>? blockDisposeForCameraIds,
|
|
Set<int>? blockInitializeForCameraIds,
|
|
}) : _blockDisposeForCameraIds = blockDisposeForCameraIds ?? const {},
|
|
_blockInitializeForCameraIds = blockInitializeForCameraIds ?? const {};
|
|
|
|
final Set<int> _blockDisposeForCameraIds;
|
|
final Set<int> _blockInitializeForCameraIds;
|
|
final _disposeCompleters = <int, Completer<void>>{};
|
|
final _initializeCompleters = <int, Completer<void>>{};
|
|
final _initializedControllers =
|
|
<int, StreamController<CameraInitializedEvent>>{};
|
|
final _errorControllers = <int, StreamController<CameraErrorEvent>>{};
|
|
final _orientationController =
|
|
StreamController<DeviceOrientationChangedEvent>.broadcast();
|
|
final createdCameraIds = <int>[];
|
|
final disposedCameraIds = <int>[];
|
|
var _nextCameraId = 1;
|
|
|
|
@override
|
|
Future<List<CameraDescription>> availableCameras() async => const [
|
|
CameraDescription(
|
|
name: 'front',
|
|
lensDirection: CameraLensDirection.front,
|
|
sensorOrientation: 0,
|
|
),
|
|
];
|
|
|
|
@override
|
|
Future<int> createCameraWithSettings(
|
|
CameraDescription description,
|
|
MediaSettings mediaSettings,
|
|
) async {
|
|
final cameraId = _nextCameraId++;
|
|
createdCameraIds.add(cameraId);
|
|
_initializedControllers[cameraId] =
|
|
StreamController<CameraInitializedEvent>.broadcast();
|
|
_errorControllers[cameraId] =
|
|
StreamController<CameraErrorEvent>.broadcast();
|
|
return cameraId;
|
|
}
|
|
|
|
@override
|
|
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
|
|
_initializedControllers[cameraId]!.stream.asBroadcastStream();
|
|
|
|
@override
|
|
Stream<CameraErrorEvent> onCameraError(int cameraId) =>
|
|
_errorControllers[cameraId]!.stream.asBroadcastStream();
|
|
|
|
@override
|
|
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() =>
|
|
_orientationController.stream;
|
|
|
|
@override
|
|
Future<void> initializeCamera(
|
|
int cameraId, {
|
|
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
|
|
}) async {
|
|
if (_blockInitializeForCameraIds.contains(cameraId)) {
|
|
await (_initializeCompleters[cameraId] ??= Completer<void>()).future;
|
|
}
|
|
_initializedControllers[cameraId]!.add(
|
|
CameraInitializedEvent(
|
|
cameraId,
|
|
640,
|
|
480,
|
|
ExposureMode.auto,
|
|
false,
|
|
FocusMode.auto,
|
|
false,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> lockCaptureOrientation(
|
|
int cameraId,
|
|
DeviceOrientation orientation,
|
|
) async {}
|
|
|
|
@override
|
|
Widget buildPreview(int cameraId) => const SizedBox.expand();
|
|
|
|
@override
|
|
Future<void> dispose(int cameraId) async {
|
|
disposedCameraIds.add(cameraId);
|
|
if (_blockDisposeForCameraIds.contains(cameraId)) {
|
|
await (_disposeCompleters[cameraId] ??= Completer<void>()).future;
|
|
}
|
|
}
|
|
|
|
void completeInitialize(int cameraId) {
|
|
_initializeCompleters[cameraId]!.complete();
|
|
}
|
|
|
|
void failDispose(int cameraId) {
|
|
_disposeCompleters[cameraId]!.completeError(
|
|
PlatformException(code: 'dispose-failed'),
|
|
);
|
|
}
|
|
|
|
void completeDispose(int cameraId) {
|
|
_disposeCompleters[cameraId]!.complete();
|
|
}
|
|
}
|