diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart index e6533042..4dad7fc0 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart @@ -168,6 +168,7 @@ class _CameraPreviewViewState extends State { super.initState(); initVolumeControl(); initAsync(); + _checkAndInitCamera(); } @override @@ -176,12 +177,26 @@ class _CameraPreviewViewState extends State { if (oldWidget.isVisible != widget.isVisible) { if (widget.isVisible) { initVolumeControl(); + _checkAndInitCamera(); } else { _deInitVolumeControl(); } } } + void _checkAndInitCamera() { + if (widget.isVisible && + mc.cameraController == null && + !mc.initCameraStarted) { + unawaited( + mc.selectCamera( + mc.selectedCameraDetails.cameraId, + false, + ), + ); + } + } + @override void dispose() { _videoRecordingTimer?.cancel(); @@ -686,7 +701,23 @@ class _CameraPreviewViewState extends State { Widget build(BuildContext context) { if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) { - return Container(); + if (widget.isVisible && !mc.initCameraStarted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAndInitCamera(); + }); + } + // Show a loader instead of an empty container to prevent a black screen + // while the camera is initializing or if initialization failed. + return Center( + child: SizedBox( + height: 60, + width: 60, + child: ThreeRotatingDots( + size: 40, + color: context.color.primary, + ), + ), + ); } return StreamBuilder( stream: userService.onUserUpdated, diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 551f0869..4fc2dca5 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -155,6 +155,7 @@ class MainCameraController { } catch (e) { Log.error('Error querying available cameras: $e'); initCameraStarted = false; + setState?.call(); return; } @@ -164,6 +165,7 @@ class MainCameraController { 'Trying to select a non existing camera $cameraId >= ${AppEnvironment.cameras.length}', ); initCameraStarted = false; + setState?.call(); return; } @@ -192,7 +194,7 @@ class MainCameraController { final hasMic = await micPermissionFuture; if (sessionId != _cameraSessionId) return; - final controller = CameraController( + var controller = CameraController( AppEnvironment.cameras[cameraId], ResolutionPreset.high, enableAudio: hasMic, @@ -204,7 +206,21 @@ class MainCameraController { var assignedToGlobal = false; try { _initializeFuture = controller.initialize(); - await _initializeFuture; + try { + await _initializeFuture; + } on CameraException catch (e) { + // If specific image format is unsupported on this hardware, fallback to default format + Log.warn('Initial camera format initialization failed ($e), trying fallback format...'); + await controller.dispose(); + controller = CameraController( + AppEnvironment.cameras[cameraId], + ResolutionPreset.high, + enableAudio: hasMic, + ); + _initializeFuture = controller.initialize(); + await _initializeFuture; + } + if (sessionId != _cameraSessionId) { unawaited(controller.dispose()); return; @@ -256,6 +272,7 @@ class MainCameraController { unawaited(controller.dispose()); } initCameraStarted = false; + setState?.call(); return; } } else { diff --git a/lib/src/visual/views/camera/camera_qr_scanner.view.dart b/lib/src/visual/views/camera/camera_qr_scanner.view.dart index c5f45f9f..58b17452 100644 --- a/lib/src/visual/views/camera/camera_qr_scanner.view.dart +++ b/lib/src/visual/views/camera/camera_qr_scanner.view.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/key_verification.service.dart'; @@ -8,6 +10,7 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/action_button.dart'; class QrCodeScannerView extends StatefulWidget { const QrCodeScannerView({ @@ -41,6 +44,7 @@ class QrCodeScannerViewState extends State { } Permission.camera.isGranted.then((hasPermission) { if (hasPermission && mounted) { + AppState.hasCameraPermissions = true; unawaited(_mainCameraController.selectCamera(0, true)); } }); @@ -100,6 +104,17 @@ class QrCodeScannerViewState extends State { ), ), ), + // Always-visible back button so the user can exit regardless of + // camera state (e.g. during init or after init failure). + Positioned( + left: 5, + top: MediaQuery.paddingOf(context).top + 10, + child: ActionButton( + FontAwesomeIcons.xmark, + tooltipText: context.lang.close, + onPressed: () => Navigator.pop(context), + ), + ), ], ), ); diff --git a/lib/src/visual/views/camera/camera_send_to.view.dart b/lib/src/visual/views/camera/camera_send_to.view.dart index 389c2423..e482954e 100644 --- a/lib/src/visual/views/camera/camera_send_to.view.dart +++ b/lib/src/visual/views/camera/camera_send_to.view.dart @@ -1,10 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart'; import 'package:twonly/src/visual/views/camera/camera_preview_components/main_camera_controller.dart'; +import 'package:twonly/src/visual/views/camera/share_image_editor_components/action_button.dart'; class CameraSendToView extends StatefulWidget { const CameraSendToView(this.sendToGroup, {super.key}); @@ -24,6 +28,7 @@ class CameraSendToViewState extends State { }; Permission.camera.isGranted.then((hasPermission) { if (hasPermission && mounted) { + AppState.hasCameraPermissions = true; unawaited(_mainCameraController.selectCamera(0, true)); } }); @@ -58,6 +63,17 @@ class CameraSendToViewState extends State { isVisible: true, ), ), + // Always-visible back button so the user can exit regardless of + // camera state (e.g. during init or after init failure). + Positioned( + left: 5, + top: MediaQuery.paddingOf(context).top + 10, + child: ActionButton( + FontAwesomeIcons.xmark, + tooltipText: context.lang.close, + onPressed: () => Navigator.pop(context), + ), + ), ], ), );