splitting the logic for #327

This commit is contained in:
otsmr 2025-12-07 18:12:59 +01:00
parent 3265b6259c
commit b3ec419411
6 changed files with 266 additions and 357 deletions

View file

@ -1,23 +1,21 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
class HomeViewCameraPreview extends StatelessWidget { class MainCameraPreview extends StatelessWidget {
const HomeViewCameraPreview({ const MainCameraPreview({
required this.controller, required this.mainCameraController,
required this.screenshotController,
required this.customPaint,
super.key, super.key,
}); });
final CameraController? controller; final MainCameraController mainCameraController;
final CustomPaint? customPaint;
final ScreenshotController screenshotController;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (controller == null || !controller!.value.isInitialized) { if (mainCameraController.cameraController == null ||
!mainCameraController.cameraController!.value.isInitialized) {
return Container(); return Container();
} }
return Positioned.fill( return Positioned.fill(
@ -26,58 +24,21 @@ class HomeViewCameraPreview extends StatelessWidget {
additionalPadding: 59, additionalPadding: 59,
bottomNavigation: Container(), bottomNavigation: Container(),
child: Screenshot( child: Screenshot(
controller: screenshotController, controller: mainCameraController.screenshotController,
child: AspectRatio( child: AspectRatio(
aspectRatio: 9 / 16, aspectRatio: 9 / 16,
child: ClipRect( child: ClipRect(
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: SizedBox( child: SizedBox(
width: controller!.value.previewSize!.height, width: mainCameraController
height: controller!.value.previewSize!.width, .cameraController!.value.previewSize!.height,
child: CameraPreview(controller!, child: customPaint), height: mainCameraController
), .cameraController!.value.previewSize!.width,
), child: CameraPreview(
), mainCameraController.cameraController!,
), child: mainCameraController.customPaint,
), ),
),
);
}
}
class SendToCameraPreview extends StatelessWidget {
const SendToCameraPreview({
required this.cameraController,
required this.screenshotController,
required this.customPaint,
super.key,
});
final CameraController? cameraController;
final ScreenshotController screenshotController;
final CustomPaint? customPaint;
@override
Widget build(BuildContext context) {
if (cameraController == null || !cameraController!.value.isInitialized) {
return Container();
}
return Positioned.fill(
child: MediaViewSizing(
requiredHeight: 0,
additionalPadding: 59,
child: Screenshot(
controller: screenshotController,
child: AspectRatio(
aspectRatio: 9 / 16,
child: ClipRect(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: cameraController!.value.previewSize!.height,
height: cameraController!.value.previewSize!.width,
child: CameraPreview(cameraController!, child: customPaint),
), ),
), ),
), ),

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,7 +10,6 @@ import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -22,6 +22,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart';
import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart'; import 'package:twonly/src/views/camera/camera_preview_components/video_recording_time.dart';
import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart';
import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
@ -89,20 +90,14 @@ class SelectedCameraDetails {
class CameraPreviewControllerView extends StatelessWidget { class CameraPreviewControllerView extends StatelessWidget {
const CameraPreviewControllerView({ const CameraPreviewControllerView({
required this.cameraController, required this.mainController,
required this.selectCamera,
required this.selectedCameraDetails,
required this.screenshotController,
required this.isVisible, required this.isVisible,
super.key, super.key,
this.sendToGroup, this.sendToGroup,
}); });
final MainCameraController mainController;
final Group? sendToGroup; final Group? sendToGroup;
final Future<CameraController?> Function(int sCameraId, bool init)
selectCamera;
final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController;
final bool isVisible; final bool isVisible;
@override @override
@ -114,16 +109,13 @@ class CameraPreviewControllerView extends StatelessWidget {
if (snap.data!) { if (snap.data!) {
return CameraPreviewView( return CameraPreviewView(
sendToGroup: sendToGroup, sendToGroup: sendToGroup,
selectCamera: selectCamera, mainCameraController: mainController,
cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails,
screenshotController: screenshotController,
isVisible: isVisible, isVisible: isVisible,
); );
} else { } else {
return PermissionHandlerView( return PermissionHandlerView(
onSuccess: () { onSuccess: () {
selectCamera(0, true); mainController.selectCamera(0, true);
}, },
); );
} }
@ -137,22 +129,14 @@ class CameraPreviewControllerView extends StatelessWidget {
class CameraPreviewView extends StatefulWidget { class CameraPreviewView extends StatefulWidget {
const CameraPreviewView({ const CameraPreviewView({
required this.selectCamera, required this.mainCameraController,
required this.cameraController,
required this.selectedCameraDetails,
required this.screenshotController,
required this.isVisible, required this.isVisible,
super.key, super.key,
this.sendToGroup, this.sendToGroup,
}); });
final MainCameraController mainCameraController;
final Group? sendToGroup; final Group? sendToGroup;
final Future<CameraController?> Function(
int sCameraId,
bool init,
) selectCamera;
final CameraController? cameraController;
final SelectedCameraDetails selectedCameraDetails;
final ScreenshotController screenshotController;
final bool isVisible; final bool isVisible;
@override @override
@ -174,6 +158,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
final GlobalKey keyTriggerButton = GlobalKey(); final GlobalKey keyTriggerButton = GlobalKey();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
MainCameraController get mc => widget.mainCameraController;
StreamSubscription<HardwareButton>? androidVolumeDownSub; StreamSubscription<HardwareButton>? androidVolumeDownSub;
@override @override
@ -282,18 +268,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<void> updateScaleFactor(double newScale) async { Future<void> updateScaleFactor(double newScale) async {
if (widget.selectedCameraDetails.scaleFactor == newScale || if (mc.selectedCameraDetails.scaleFactor == newScale ||
widget.cameraController == null) { mc.cameraController == null) {
return; return;
} }
await widget.cameraController?.setZoomLevel( await mc.cameraController?.setZoomLevel(
newScale.clamp( newScale.clamp(
widget.selectedCameraDetails.minAvailableZoom, mc.selectedCameraDetails.minAvailableZoom,
widget.selectedCameraDetails.maxAvailableZoom, mc.selectedCameraDetails.maxAvailableZoom,
), ),
); );
setState(() { setState(() {
widget.selectedCameraDetails.scaleFactor = newScale; mc.selectedCameraDetails.scaleFactor = newScale;
}); });
} }
@ -325,31 +311,31 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
setState(() { setState(() {
_sharePreviewIsShown = true; _sharePreviewIsShown = true;
}); });
if (widget.selectedCameraDetails.isFlashOn) { if (mc.selectedCameraDetails.isFlashOn) {
if (isFront) { if (isFront) {
setState(() { setState(() {
_showSelfieFlash = true; _showSelfieFlash = true;
}); });
} else { } else {
await widget.cameraController?.setFlashMode(FlashMode.torch); await mc.cameraController?.setFlashMode(FlashMode.torch);
} }
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
} }
await widget.cameraController?.pausePreview(); await mc.cameraController?.pausePreview();
if (!mounted) { if (!mounted) {
return; return;
} }
if (Platform.isIOS) { if (Platform.isIOS) {
// android has a problem with this. Flash is turned off in the pausePreview function. // android has a problem with this. Flash is turned off in the pausePreview function.
await widget.cameraController?.setFlashMode(FlashMode.off); await mc.cameraController?.setFlashMode(FlashMode.off);
} }
if (!mounted) { if (!mounted) {
return; return;
} }
imageBytes = widget.screenshotController imageBytes = mc.screenshotController
.capture(pixelRatio: MediaQuery.of(context).devicePixelRatio); .capture(pixelRatio: MediaQuery.of(context).devicePixelRatio);
if (await pushMediaEditor(imageBytes, null)) { if (await pushMediaEditor(imageBytes, null)) {
@ -426,33 +412,33 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
return true; return true;
} }
await widget.selectCamera( await mc.selectCamera(
widget.selectedCameraDetails.cameraId, mc.selectedCameraDetails.cameraId,
false, false,
); );
return false; return false;
} }
bool get isFront => bool get isFront =>
widget.cameraController?.description.lensDirection == mc.cameraController?.description.lensDirection ==
CameraLensDirection.front; CameraLensDirection.front;
Future<void> onPanUpdate(dynamic details) async { Future<void> onPanUpdate(dynamic details) async {
if (isFront || details == null) { if (isFront || details == null) {
return; return;
} }
if (widget.cameraController == null || if (mc.cameraController == null ||
!widget.cameraController!.value.isInitialized) { !mc.cameraController!.value.isInitialized) {
return; return;
} }
widget.selectedCameraDetails.scaleFactor = (_baseScaleFactor + mc.selectedCameraDetails.scaleFactor = (_baseScaleFactor +
// ignore: avoid_dynamic_calls // ignore: avoid_dynamic_calls
(_basePanY - (details.localPosition.dy as double)) / 30) (_basePanY - (details.localPosition.dy as double)) / 30)
.clamp(1, widget.selectedCameraDetails.maxAvailableZoom); .clamp(1, mc.selectedCameraDetails.maxAvailableZoom);
await widget.cameraController! await mc.cameraController!
.setZoomLevel(widget.selectedCameraDetails.scaleFactor); .setZoomLevel(mc.selectedCameraDetails.scaleFactor);
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
@ -509,8 +495,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
Future<void> startVideoRecording() async { Future<void> startVideoRecording() async {
if (widget.cameraController != null && if (mc.cameraController != null &&
widget.cameraController!.value.isRecordingVideo) { mc.cameraController!.value.isRecordingVideo) {
return; return;
} }
setState(() { setState(() {
@ -518,7 +504,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
try { try {
await widget.cameraController?.startVideoRecording(); await mc.cameraController?.startVideoRecording();
_videoRecordingTimer = _videoRecordingTimer =
Timer.periodic(const Duration(milliseconds: 15), (timer) { Timer.periodic(const Duration(milliseconds: 15), (timer) {
setState(() { setState(() {
@ -556,8 +542,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
_isVideoRecording = false; _isVideoRecording = false;
}); });
if (widget.cameraController == null || if (mc.cameraController == null ||
!widget.cameraController!.value.isRecordingVideo) { !mc.cameraController!.value.isRecordingVideo) {
return; return;
} }
@ -566,9 +552,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
try { try {
final videoPath = await widget.cameraController?.stopVideoRecording(); final videoPath = await mc.cameraController?.stopVideoRecording();
if (videoPath == null) return; if (videoPath == null) return;
await widget.cameraController?.pausePreview(); await mc.cameraController?.pausePreview();
if (await pushMediaEditor(null, File(videoPath.path))) { if (await pushMediaEditor(null, File(videoPath.path))) {
return; return;
} }
@ -595,8 +581,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.selectedCameraDetails.cameraId >= gCameras.length || if (mc.selectedCameraDetails.cameraId >= gCameras.length ||
widget.cameraController == null) { mc.cameraController == null) {
return Container(); return Container();
} }
return MediaViewSizing( return MediaViewSizing(
@ -610,14 +596,14 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
} }
setState(() { setState(() {
_basePanY = details.localPosition.dy; _basePanY = details.localPosition.dy;
_baseScaleFactor = widget.selectedCameraDetails.scaleFactor; _baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
}); });
}, },
onLongPressMoveUpdate: onPanUpdate, onLongPressMoveUpdate: onPanUpdate,
onLongPressStart: (details) { onLongPressStart: (details) {
setState(() { setState(() {
_basePanY = details.localPosition.dy; _basePanY = details.localPosition.dy;
_baseScaleFactor = widget.selectedCameraDetails.scaleFactor; _baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
}); });
// Get the position of the pointer // Get the position of the pointer
final renderBox = final renderBox =
@ -670,29 +656,29 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
Icons.repeat_rounded, Icons.repeat_rounded,
tooltipText: context.lang.switchFrontAndBackCamera, tooltipText: context.lang.switchFrontAndBackCamera,
onPressed: () async { onPressed: () async {
await widget.selectCamera( await mc.selectCamera(
(widget.selectedCameraDetails.cameraId + 1) % 2, (mc.selectedCameraDetails.cameraId + 1) % 2,
false, false,
); );
}, },
), ),
ActionButton( ActionButton(
widget.selectedCameraDetails.isFlashOn mc.selectedCameraDetails.isFlashOn
? Icons.flash_on_rounded ? Icons.flash_on_rounded
: Icons.flash_off_rounded, : Icons.flash_off_rounded,
tooltipText: context.lang.toggleFlashLight, tooltipText: context.lang.toggleFlashLight,
color: widget.selectedCameraDetails.isFlashOn color: mc.selectedCameraDetails.isFlashOn
? Colors.white ? Colors.white
: Colors.white.withAlpha(160), : Colors.white.withAlpha(160),
onPressed: () async { onPressed: () async {
if (widget.selectedCameraDetails.isFlashOn) { if (mc.selectedCameraDetails.isFlashOn) {
await widget.cameraController await mc.cameraController
?.setFlashMode(FlashMode.off); ?.setFlashMode(FlashMode.off);
widget.selectedCameraDetails.isFlashOn = false; mc.selectedCameraDetails.isFlashOn = false;
} else { } else {
await widget.cameraController await mc.cameraController
?.setFlashMode(FlashMode.always); ?.setFlashMode(FlashMode.always);
widget.selectedCameraDetails.isFlashOn = true; mc.selectedCameraDetails.isFlashOn = true;
} }
setState(() {}); setState(() {});
}, },
@ -719,20 +705,19 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Column( child: Column(
children: [ children: [
if (widget.cameraController!.value.isInitialized && if (mc.cameraController!.value.isInitialized &&
widget.selectedCameraDetails.isZoomAble && mc.selectedCameraDetails.isZoomAble &&
!isFront && !isFront &&
!_isVideoRecording) !_isVideoRecording)
SizedBox( SizedBox(
width: 120, width: 120,
child: CameraZoomButtons( child: CameraZoomButtons(
key: widget.key, key: widget.key,
scaleFactor: scaleFactor: mc.selectedCameraDetails.scaleFactor,
widget.selectedCameraDetails.scaleFactor,
updateScaleFactor: updateScaleFactor, updateScaleFactor: updateScaleFactor,
selectCamera: widget.selectCamera, selectCamera: mc.selectCamera,
selectedCameraDetails: widget.selectedCameraDetails, selectedCameraDetails: mc.selectedCameraDetails,
controller: widget.cameraController!, controller: mc.cameraController!,
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),

View file

@ -0,0 +1,155 @@
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart';
class MainCameraController {
late void Function() setState;
CameraController? cameraController;
ScreenshotController screenshotController = ScreenshotController();
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
bool initCameraStarted = true;
Future<void> closeCamera() async {
await cameraController?.stopImageStream();
await cameraController?.dispose();
cameraController = null;
initCameraStarted = false;
selectedCameraDetails = SelectedCameraDetails();
}
Future<CameraController?> selectCamera(int sCameraId, bool init) async {
initCameraStarted = true;
final opts = await initializeCameraController(
selectedCameraDetails,
sCameraId,
init,
);
if (opts != null) {
selectedCameraDetails = opts.$1;
cameraController = opts.$2;
}
if (cameraController?.description.lensDirection ==
CameraLensDirection.back) {
await cameraController?.startImageStream(_processCameraImage);
}
setState();
return cameraController;
}
Future<void> toggleSelectedCamera() async {
if (cameraController == null) return;
// do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo) {
return;
}
await cameraController!.stopImageStream();
await cameraController!.dispose();
cameraController = null;
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
final BarcodeScanner _barcodeScanner = BarcodeScanner();
bool _isBusy = false;
CustomPaint? customPaint;
final Map<DeviceOrientation, int> _orientations = {
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeLeft: 90,
DeviceOrientation.portraitDown: 180,
DeviceOrientation.landscapeRight: 270,
};
void _processCameraImage(CameraImage image) {
final inputImage = _inputImageFromCameraImage(image);
if (inputImage == null) return;
_processImage(inputImage);
}
InputImage? _inputImageFromCameraImage(CameraImage image) {
if (cameraController == null) return null;
// get image rotation
// it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java
// `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m
// in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart
final camera = cameraController!.description;
final sensorOrientation = camera.sensorOrientation;
// print(
// 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}');
InputImageRotation? rotation;
if (Platform.isIOS) {
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
} else if (Platform.isAndroid) {
var rotationCompensation =
_orientations[cameraController!.value.deviceOrientation];
if (rotationCompensation == null) return null;
if (camera.lensDirection == CameraLensDirection.front) {
// front-facing
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
} else {
// back-facing
rotationCompensation =
(sensorOrientation - rotationCompensation + 360) % 360;
}
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
// print('rotationCompensation: $rotationCompensation');
}
if (rotation == null) return null;
// print('final rotation: $rotation');
// get image format
var format = InputImageFormatValue.fromRawValue(image.format.raw as int);
// validate format depending on platform
// only supported formats:
// * nv21 for Android
// * bgra8888 for iOS
if (Platform.isAndroid && format == InputImageFormat.yuv420) {
// https://developer.android.com/reference/kotlin/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21()
format = InputImageFormat.nv21;
}
if (format == null ||
(Platform.isAndroid && format != InputImageFormat.nv21) ||
(Platform.isIOS && format != InputImageFormat.bgra8888)) {
return null;
}
// since format is constraint to nv21 or bgra8888, both only have one plane
if (image.planes.length != 1) return null;
final plane = image.planes.first;
// compose InputImage using bytes
return InputImage.fromBytes(
bytes: plane.bytes,
metadata: InputImageMetadata(
size: Size(image.width.toDouble(), image.height.toDouble()),
rotation: rotation, // used only in Android
format: format, // used only in iOS
bytesPerRow: plane.bytesPerRow, // used only in iOS
),
);
}
Future<void> _processImage(InputImage inputImage) async {
if (_isBusy) return;
_isBusy = true;
final barcodes = await _barcodeScanner.processImage(inputImage);
if (inputImage.metadata?.size != null &&
inputImage.metadata?.rotation != null &&
cameraController != null) {
final painter = BarcodeDetectorPainter(
barcodes,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
);
customPaint = CustomPaint(painter: painter);
}
_isBusy = false;
setState();
}
}

View file

@ -6,7 +6,7 @@ import 'dart:math';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
class CameraZoomButtons extends StatefulWidget { class CameraZoomButtons extends StatefulWidget {
const CameraZoomButtons({ const CameraZoomButtons({

View file

@ -1,11 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
class CameraSendToView extends StatefulWidget { class CameraSendToView extends StatefulWidget {
const CameraSendToView(this.sendToGroup, {super.key}); const CameraSendToView(this.sendToGroup, {super.key});
@ -15,71 +13,36 @@ class CameraSendToView extends StatefulWidget {
} }
class CameraSendToViewState extends State<CameraSendToView> { class CameraSendToViewState extends State<CameraSendToView> {
CameraController? cameraController; final MainCameraController _mainCameraController = MainCameraController();
ScreenshotController screenshotController = ScreenshotController();
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
unawaited(selectCamera(0, true)); _mainCameraController.setState = () {
if (mounted) setState(() {});
};
unawaited(_mainCameraController.selectCamera(0, true));
} }
@override @override
void dispose() { void dispose() {
cameraController?.dispose(); _mainCameraController.closeCamera();
cameraController = null;
selectedCameraDetails = SelectedCameraDetails();
super.dispose(); super.dispose();
} }
Future<CameraController?> selectCamera(
int sCameraId,
bool init,
) async {
final opts = await initializeCameraController(
selectedCameraDetails,
sCameraId,
init,
);
if (opts != null) {
selectedCameraDetails = opts.$1;
cameraController = opts.$2;
}
setState(() {});
return cameraController;
}
/// same function also in home.view.dart
Future<void> toggleSelectedCamera() async {
if (cameraController == null) return;
// do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo) {
return;
}
await cameraController!.dispose();
cameraController = null;
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: toggleSelectedCamera, onDoubleTap: _mainCameraController.toggleSelectedCamera,
child: Stack( child: Stack(
children: [ children: [
SendToCameraPreview( MainCameraPreview(
cameraController: cameraController, mainCameraController: _mainCameraController,
screenshotController: screenshotController,
customPaint: null,
), ),
CameraPreviewControllerView( CameraPreviewControllerView(
selectCamera: selectCamera, mainController: _mainCameraController,
sendToGroup: widget.sendToGroup, sendToGroup: widget.sendToGroup,
cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails,
screenshotController: screenshotController,
isVisible: true, isVisible: true,
), ),
], ],

View file

@ -1,19 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart';
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview_controller_view.dart';
import 'package:twonly/src/views/camera/painters/barcode_detector_painter.dart'; import 'package:twonly/src/views/camera/camera_preview_components/main_camera_controller.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart';
import 'package:twonly/src/views/memories/memories.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart';
@ -51,6 +46,8 @@ class Shade extends StatelessWidget {
class HomeViewState extends State<HomeView> { class HomeViewState extends State<HomeView> {
int activePageIdx = 0; int activePageIdx = 0;
final MainCameraController _mainCameraController = MainCameraController();
final PageController homeViewPageController = PageController(initialPage: 1); final PageController homeViewPageController = PageController(initialPage: 1);
double buttonDiameter = 100; double buttonDiameter = 100;
@ -59,11 +56,6 @@ class HomeViewState extends State<HomeView> {
double lastChange = 0; double lastChange = 0;
Timer? disableCameraTimer; Timer? disableCameraTimer;
bool initCameraStarted = true;
CameraController? cameraController;
ScreenshotController screenshotController = ScreenshotController();
SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails();
bool onPageView(ScrollNotification notification) { bool onPageView(ScrollNotification notification) {
disableCameraTimer?.cancel(); disableCameraTimer?.cancel();
@ -75,15 +67,19 @@ class HomeViewState extends State<HomeView> {
offsetRatio = offsetFromOne.abs(); offsetRatio = offsetFromOne.abs();
}); });
} }
if (cameraController == null && !initCameraStarted && offsetRatio < 1) { if (_mainCameraController.cameraController == null &&
initCameraStarted = true; !_mainCameraController.initCameraStarted &&
unawaited(selectCamera(selectedCameraDetails.cameraId, false)); offsetRatio < 1) {
unawaited(
_mainCameraController.selectCamera(
_mainCameraController.selectedCameraDetails.cameraId,
false,
),
);
} }
if (offsetRatio == 1) { if (offsetRatio == 1) {
disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async {
await cameraController?.dispose(); await _mainCameraController.closeCamera();
cameraController = null;
selectedCameraDetails = SelectedCameraDetails();
disableCameraTimer = null; disableCameraTimer = null;
}); });
} }
@ -93,6 +89,9 @@ class HomeViewState extends State<HomeView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_mainCameraController.setState = () {
if (mounted) setState(() {});
};
activePageIdx = widget.initialPage; activePageIdx = widget.initialPage;
globalUpdateOfHomeViewPageIndex = (index) { globalUpdateOfHomeViewPageIndex = (index) {
homeViewPageController.jumpToPage(index); homeViewPageController.jumpToPage(index);
@ -104,7 +103,7 @@ class HomeViewState extends State<HomeView> {
.listen((NotificationResponse? response) async { .listen((NotificationResponse? response) async {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
}); });
unawaited(selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
unawaited(initAsync()); unawaited(initAsync());
} }
@ -112,159 +111,10 @@ class HomeViewState extends State<HomeView> {
void dispose() { void dispose() {
unawaited(selectNotificationStream.close()); unawaited(selectNotificationStream.close());
disableCameraTimer?.cancel(); disableCameraTimer?.cancel();
cameraController?.stopImageStream(); _mainCameraController.closeCamera();
cameraController?.dispose();
cameraController = null;
super.dispose(); super.dispose();
} }
Future<CameraController?> selectCamera(int sCameraId, bool init) async {
final opts = await initializeCameraController(
selectedCameraDetails,
sCameraId,
init,
);
if (opts != null) {
selectedCameraDetails = opts.$1;
cameraController = opts.$2;
initCameraStarted = false;
}
if (cameraController?.description.lensDirection ==
CameraLensDirection.back) {
await cameraController?.startImageStream(_processCameraImage);
}
setState(() {});
return cameraController;
}
/// same function also in camera_send_to_view
Future<void> toggleSelectedCamera() async {
if (cameraController == null) return;
// do not allow switching camera when recording
if (cameraController!.value.isRecordingVideo) {
return;
}
await cameraController!.stopImageStream();
await cameraController!.dispose();
cameraController = null;
await selectCamera((selectedCameraDetails.cameraId + 1) % 2, false);
}
final BarcodeScanner _barcodeScanner = BarcodeScanner();
bool _canProcess = true;
bool _isBusy = false;
CustomPaint? _customPaint;
String? _text;
final Map<DeviceOrientation, int> _orientations = {
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeLeft: 90,
DeviceOrientation.portraitDown: 180,
DeviceOrientation.landscapeRight: 270,
};
void _processCameraImage(CameraImage image) {
final inputImage = _inputImageFromCameraImage(image);
if (inputImage == null) return;
_processImage(inputImage);
}
InputImage? _inputImageFromCameraImage(CameraImage image) {
if (cameraController == null) return null;
// get image rotation
// it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java
// `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m
// in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart
final camera = cameraController!.description;
final sensorOrientation = camera.sensorOrientation;
// print(
// 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}');
InputImageRotation? rotation;
if (Platform.isIOS) {
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
} else if (Platform.isAndroid) {
var rotationCompensation =
_orientations[cameraController!.value.deviceOrientation];
if (rotationCompensation == null) return null;
if (camera.lensDirection == CameraLensDirection.front) {
// front-facing
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
} else {
// back-facing
rotationCompensation =
(sensorOrientation - rotationCompensation + 360) % 360;
}
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
// print('rotationCompensation: $rotationCompensation');
}
if (rotation == null) return null;
// print('final rotation: $rotation');
// get image format
var format = InputImageFormatValue.fromRawValue(image.format.raw as int);
// validate format depending on platform
// only supported formats:
// * nv21 for Android
// * bgra8888 for iOS
if (Platform.isAndroid && format == InputImageFormat.yuv420) {
// https://developer.android.com/reference/kotlin/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21()
format = InputImageFormat.nv21;
}
if (format == null ||
(Platform.isAndroid && format != InputImageFormat.nv21) ||
(Platform.isIOS && format != InputImageFormat.bgra8888)) {
return null;
}
// since format is constraint to nv21 or bgra8888, both only have one plane
if (image.planes.length != 1) return null;
final plane = image.planes.first;
// compose InputImage using bytes
return InputImage.fromBytes(
bytes: plane.bytes,
metadata: InputImageMetadata(
size: Size(image.width.toDouble(), image.height.toDouble()),
rotation: rotation, // used only in Android
format: format, // used only in iOS
bytesPerRow: plane.bytesPerRow, // used only in iOS
),
);
}
Future<void> _processImage(InputImage inputImage) async {
if (!_canProcess) return;
if (_isBusy) return;
_isBusy = true;
setState(() {
_text = '';
});
final barcodes = await _barcodeScanner.processImage(inputImage);
if (inputImage.metadata?.size != null &&
inputImage.metadata?.rotation != null &&
cameraController != null) {
final painter = BarcodeDetectorPainter(
barcodes,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection);
_customPaint = CustomPaint(painter: painter);
} else {
String text = 'Barcodes found: ${barcodes.length}\n\n';
for (final barcode in barcodes) {
text += 'Barcode: ${barcode.rawValue}\n\n';
}
_text = text;
// TODO: set _customPaint to draw boundingRect on top of image
_customPaint = null;
}
_isBusy = false;
if (mounted) {
setState(() {});
}
}
Future<void> initAsync() async { Future<void> initAsync() async {
final notificationAppLaunchDetails = final notificationAppLaunchDetails =
await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
@ -294,14 +144,12 @@ class HomeViewState extends State<HomeView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, onDoubleTap: offsetRatio == 0
? _mainCameraController.toggleSelectedCamera
: null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
HomeViewCameraPreview( MainCameraPreview(mainCameraController: _mainCameraController),
controller: cameraController,
screenshotController: screenshotController,
customPaint: _customPaint,
),
Shade( Shade(
opacity: offsetRatio, opacity: offsetRatio,
), ),
@ -333,10 +181,7 @@ class HomeViewState extends State<HomeView> {
child: Opacity( child: Opacity(
opacity: 1 - (offsetRatio * 4) % 1, opacity: 1 - (offsetRatio * 4) % 1,
child: CameraPreviewControllerView( child: CameraPreviewControllerView(
cameraController: cameraController, mainController: _mainCameraController,
screenshotController: screenshotController,
selectedCameraDetails: selectedCameraDetails,
selectCamera: selectCamera,
isVisible: isVisible:
((1 - (offsetRatio * 4) % 1) == 1) && activePageIdx == 1, ((1 - (offsetRatio * 4) % 1) == 1) && activePageIdx == 1,
), ),