diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart new file mode 100644 index 0000000..d077825 --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -0,0 +1,54 @@ +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:twonly/src/views/components/media_view_sizing.dart'; +import 'package:twonly/src/views/home_view.dart'; + +class CameraPreviewWidget extends StatefulWidget { + const CameraPreviewWidget({ + super.key, + }); + + @override + State createState() => _CameraPreviewWidgetState(); +} + +class _CameraPreviewWidgetState extends State { + @override + Widget build(BuildContext context) { + if (HomeViewState.cameraController == null || + !HomeViewState.cameraController!.value.isInitialized) { + return Container(); + } + bool isFront = HomeViewState.cameraController?.description.lensDirection == + CameraLensDirection.front; + return Positioned.fill( + child: MediaViewSizing( + child: Screenshot( + controller: HomeViewState.screenshotController, + child: AspectRatio( + aspectRatio: 9 / 16, + child: ClipRect( + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: + HomeViewState.cameraController!.value.previewSize!.height, + height: + HomeViewState.cameraController!.value.previewSize!.width, + child: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY( + (isFront && Platform.isAndroid) ? 3.14 : 0), + child: CameraPreview(HomeViewState.cameraController!), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/views/components/permissions_view.dart b/lib/src/views/camera/camera_preview_components/permissions_view.dart similarity index 100% rename from lib/src/views/components/permissions_view.dart rename to lib/src/views/camera/camera_preview_components/permissions_view.dart diff --git a/lib/src/views/camera/components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart similarity index 100% rename from lib/src/views/camera/components/save_to_gallery.dart rename to lib/src/views/camera/camera_preview_components/save_to_gallery.dart diff --git a/lib/src/views/camera/camera_preview_components/send_to.dart b/lib/src/views/camera/camera_preview_components/send_to.dart new file mode 100644 index 0000000..6992f1e --- /dev/null +++ b/lib/src/views/camera/camera_preview_components/send_to.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class SendToWidget extends StatelessWidget { + final String sendTo; + + const SendToWidget({ + super.key, + required this.sendTo, + }); + + @override + Widget build(BuildContext context) { + TextStyle textStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 24, + decoration: TextDecoration.none, + shadows: [ + Shadow( + color: const Color.fromARGB(122, 0, 0, 0), + blurRadius: 5.0, + ), + ], + ); + + TextStyle boldTextStyle = textStyle.copyWith( + fontWeight: FontWeight.normal, + fontSize: 28, + ); + + return Positioned( + right: 0, + left: 0, + top: 50, + child: Column( + children: [ + Text( + context.lang.cameraPreviewSendTo, + textAlign: TextAlign.center, + style: textStyle, + ), + Text( + sendTo, + textAlign: TextAlign.center, + style: boldTextStyle, // Use the bold text style here + ), + ], + ), + ); + } + + String getContactDisplayName(String contact) { + // Replace this with your actual logic to get the contact display name + return contact; // Placeholder implementation + } +} diff --git a/lib/src/views/camera/components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart similarity index 100% rename from lib/src/views/camera/components/zoom_selector.dart rename to lib/src/views/camera/camera_preview_components/zoom_selector.dart diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart new file mode 100644 index 0000000..c81be0c --- /dev/null +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -0,0 +1,693 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:logging/logging.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/send_to.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/zoom_selector.dart'; +import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/camera/image_editor/action_button.dart'; +import 'package:twonly/src/views/components/media_view_sizing.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart'; +import 'package:twonly/src/utils/storage.dart'; +import 'package:twonly/src/views/camera/share_image_editor_view.dart'; +import 'package:twonly/src/views/home_view.dart'; + +int maxVideoRecordingTime = 15; + +class SelectedCameraDetails { + double maxAvailableZoom = 1; + double minAvailableZoom = 1; + int cameraId = 0; + bool isZoomAble = false; + bool isFlashOn = false; + double scaleFactor = 1; + bool cameraLoaded = false; +} + +class CameraPreviewControllerView extends StatefulWidget { + const CameraPreviewControllerView({ + super.key, + required this.selectCamera, + this.sendTo, + }); + final Contact? sendTo; + final Function(int sCameraId, bool init, bool enableAudio) selectCamera; + + @override + State createState() => + _CameraPreviewControllerView(); +} + +class _CameraPreviewControllerView extends State { + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: checkPermissions(), + builder: (context, snap) { + if (snap.hasData) { + if (snap.data!) { + return CameraPreviewView( + sendTo: widget.sendTo, + selectCamera: widget.selectCamera, + ); + } else { + return PermissionHandlerView(onSuccess: () { + setState(() {}); + }); + } + } else { + return Container(); + } + }, + ); + } +} + +class CameraPreviewView extends StatefulWidget { + const CameraPreviewView({ + super.key, + this.sendTo, + required this.selectCamera, + }); + final Contact? sendTo; + final Function(int sCameraId, bool init, bool enableAudio) selectCamera; + + @override + State createState() => _CameraPreviewViewState(); +} + +class _CameraPreviewViewState extends State { + bool sharePreviewIsShown = false; + bool galleryLoadedImageIsShown = false; + bool showSelfieFlash = false; + double basePanY = 0; + double baseScaleFactor = 0; + bool cameraLoaded = false; + bool useHighQuality = false; + bool isVideoRecording = false; + bool hasAudioPermission = true; + bool videoWithAudio = true; + DateTime? videoRecordingStarted; + Timer? videoRecordingTimer; + + DateTime currentTime = DateTime.now(); + final GlobalKey keyTriggerButton = GlobalKey(); + final GlobalKey navigatorKey = GlobalKey(); + + @override + void initState() { + super.initState(); + // selectCamera(0, init: true); + initAsync(); + } + + void initAsync() async { + final user = await getUser(); + if (user == null) return; + if (user.useHighQuality != null) { + useHighQuality = user.useHighQuality!; + } + hasAudioPermission = await Permission.microphone.isGranted; + if (!mounted) return; + setState(() {}); + } + + @override + void dispose() { + videoRecordingTimer?.cancel(); + super.dispose(); + } + + Future requestMicrophonePermission() async { + Map statuses = await [ + Permission.microphone, + ].request(); + if (statuses[Permission.microphone]!.isPermanentlyDenied) { + openAppSettings(); + } else { + hasAudioPermission = await Permission.microphone.isGranted; + setState(() {}); + } + } + + Future updateScaleFactor(double newScale) async { + if (HomeViewState.selectedCameraDetails.scaleFactor == newScale || + HomeViewState.cameraController == null) return; + await HomeViewState.cameraController?.setZoomLevel(newScale.clamp( + HomeViewState.selectedCameraDetails.minAvailableZoom, + HomeViewState.selectedCameraDetails.maxAvailableZoom)); + setState(() { + HomeViewState.selectedCameraDetails.scaleFactor = newScale; + }); + } + + Future loadAndDeletePictureFromFile(XFile picture) async { + try { + // Load the image into bytes + final Uint8List imageBytes = await picture.readAsBytes(); + // Remove the image file + await File(picture.path).delete(); + return imageBytes; + } catch (e) { + if (context.mounted) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading picture: $e'), + duration: Duration(seconds: 3), + ), + ); + } + return null; + } + } + + Future takePicture() async { + if (sharePreviewIsShown || isVideoRecording) return; + late Future imageBytes; + + setState(() { + sharePreviewIsShown = true; + }); + if (HomeViewState.selectedCameraDetails.isFlashOn) { + if (isFront) { + setState(() { + showSelfieFlash = true; + }); + } else { + HomeViewState.cameraController?.setFlashMode(FlashMode.torch); + } + await Future.delayed(Duration(milliseconds: 1000)); + } + + await HomeViewState.cameraController?.pausePreview(); + if (!context.mounted) return; + + HomeViewState.cameraController?.setFlashMode( + HomeViewState.selectedCameraDetails.isFlashOn + ? FlashMode.always + : FlashMode.off); + imageBytes = HomeViewState.screenshotController.capture( + pixelRatio: + (useHighQuality) ? MediaQuery.of(context).devicePixelRatio : 1); + + if (await pushMediaEditor(imageBytes, null)) { + return; + } + } + + Future pushMediaEditor( + Future? imageBytes, File? videoFilePath) async { + bool? shoudReturn = await Navigator.push( + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (context, a1, a2) => ShareImageEditorView( + videoFilePath: videoFilePath, + imageBytes: imageBytes, + sendTo: widget.sendTo, + mirrorVideo: isFront && Platform.isAndroid, + useHighQuality: useHighQuality, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return child; + }, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + if (!context.mounted) return true; + // shouldReturn is null when the user used the back button + if (shoudReturn != null && shoudReturn) { + // ignore: use_build_context_synchronously + if (widget.sendTo == null) { + globalUpdateOfHomeViewPageIndex(0); + } else { + Navigator.pop(context); + } + return true; + } + widget.selectCamera( + HomeViewState.selectedCameraDetails.cameraId, false, false); + if (context.mounted) { + setState(() { + sharePreviewIsShown = false; + showSelfieFlash = false; + }); + } + return false; + } + + bool get isFront => + HomeViewState.cameraController?.description.lensDirection == + CameraLensDirection.front; + + Future onPanUpdate(details) async { + if (isFront) { + return; + } + if (HomeViewState.cameraController == null) return; + if (!HomeViewState.cameraController!.value.isInitialized) return; + + HomeViewState.selectedCameraDetails.scaleFactor = + (baseScaleFactor + (basePanY - details.localPosition.dy) / 30) + .clamp(1, HomeViewState.selectedCameraDetails.maxAvailableZoom); + + await HomeViewState.cameraController! + .setZoomLevel(HomeViewState.selectedCameraDetails.scaleFactor); + if (mounted) { + setState(() {}); + } + } + + Future pickImageFromGallery() async { + setState(() { + galleryLoadedImageIsShown = true; + sharePreviewIsShown = true; + }); + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + + if (pickedFile != null) { + File imageFile = File(pickedFile.path); + if (await pushMediaEditor(imageFile.readAsBytes(), null)) { + return; + } + } + setState(() { + galleryLoadedImageIsShown = false; + sharePreviewIsShown = false; + }); + } + + Future startVideoRecording() async { + if (HomeViewState.cameraController != null && + HomeViewState.cameraController!.value.isRecordingVideo) return; + if (hasAudioPermission && videoWithAudio) { + await widget.selectCamera( + HomeViewState.selectedCameraDetails.cameraId, + false, + await Permission.microphone.isGranted && videoWithAudio, + ); + } + + setState(() { + isVideoRecording = true; + }); + + try { + await HomeViewState.cameraController?.startVideoRecording(); + videoRecordingTimer = Timer.periodic(Duration(milliseconds: 15), (timer) { + setState(() { + currentTime = DateTime.now(); + }); + if (videoRecordingStarted != null && + currentTime.difference(videoRecordingStarted!).inSeconds >= + maxVideoRecordingTime) { + timer.cancel(); + videoRecordingTimer = null; + stopVideoRecording(); + } + }); + setState(() { + videoRecordingStarted = DateTime.now(); + isVideoRecording = true; + }); + } on CameraException catch (e) { + setState(() { + isVideoRecording = false; + }); + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + if (videoRecordingTimer != null) { + videoRecordingTimer?.cancel(); + videoRecordingTimer = null; + } + if (HomeViewState.cameraController == null || + !HomeViewState.cameraController!.value.isRecordingVideo) { + return null; + } + + try { + setState(() { + videoRecordingStarted = null; + isVideoRecording = false; + sharePreviewIsShown = true; + }); + File? videoPathFile; + XFile? videoPath = + await HomeViewState.cameraController?.stopVideoRecording(); + if (videoPath != null) { + if (Platform.isAndroid) { + // see https://github.com/flutter/flutter/issues/148335 + await File(videoPath.path).rename("${videoPath.path}.mp4"); + videoPathFile = File("${videoPath.path}.mp4"); + } else { + videoPathFile = File(videoPath.path); + } + } + await HomeViewState.cameraController?.pausePreview(); + if (await pushMediaEditor(null, videoPathFile)) { + return; + } + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(dynamic e) { + Logger("ui.camera").shout("$e"); + try { + if (context.mounted) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: $e'), + duration: Duration(seconds: 3), + ), + ); + } + // ignore: empty_catches + } catch (e) {} + } + + @override + Widget build(BuildContext context) { + if (HomeViewState.selectedCameraDetails.cameraId >= gCameras.length || + HomeViewState.cameraController == null) { + return Container(); + } + return MediaViewSizing( + child: GestureDetector( + onPanStart: (details) async { + if (isFront) { + return; + } + setState(() { + basePanY = details.localPosition.dy; + baseScaleFactor = HomeViewState.selectedCameraDetails.scaleFactor; + }); + }, + onLongPressMoveUpdate: onPanUpdate, + onLongPressStart: (details) { + setState(() { + basePanY = details.localPosition.dy; + baseScaleFactor = HomeViewState.selectedCameraDetails.scaleFactor; + }); + // Get the position of the pointer + RenderBox renderBox = + keyTriggerButton.currentContext?.findRenderObject() as RenderBox; + Offset localPosition = + renderBox.globalToLocal(details.globalPosition); + + final containerRect = + Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); + + if (containerRect.contains(localPosition)) { + startVideoRecording(); + } + }, + onLongPressEnd: (a) { + stopVideoRecording(); + }, + onPanEnd: (a) { + stopVideoRecording(); + }, + onPanUpdate: onPanUpdate, + child: Stack( + children: [ + // if (!galleryLoadedImageIsShown) + // CameraPreviewWidget( + // controller: HomeViewState.cameraController, + // screenshotController: screenshotController, + // ), + if (galleryLoadedImageIsShown) + Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 1, color: context.color.primary), + ), + ), + // Positioned.fill( + // child: GestureDetector(), + // ), + if (!sharePreviewIsShown && + widget.sendTo != null && + !isVideoRecording) + SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), + if (!sharePreviewIsShown && !isVideoRecording) + Positioned( + right: 5, + top: 0, + child: Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(vertical: 16), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ActionButton( + Icons.repeat_rounded, + tooltipText: context.lang.switchFrontAndBackCamera, + onPressed: () async { + widget.selectCamera( + (HomeViewState.selectedCameraDetails.cameraId + + 1) % + 2, + false, + false); + }, + ), + ActionButton( + HomeViewState.selectedCameraDetails.isFlashOn + ? Icons.flash_on_rounded + : Icons.flash_off_rounded, + tooltipText: context.lang.toggleFlashLight, + color: HomeViewState.selectedCameraDetails.isFlashOn + ? Colors.white + : Colors.white.withAlpha(160), + onPressed: () async { + if (HomeViewState.selectedCameraDetails.isFlashOn) { + HomeViewState.cameraController + ?.setFlashMode(FlashMode.off); + HomeViewState.selectedCameraDetails.isFlashOn = + false; + } else { + HomeViewState.cameraController + ?.setFlashMode(FlashMode.always); + HomeViewState.selectedCameraDetails.isFlashOn = + true; + } + setState(() {}); + }, + ), + if (!isFront) + ActionButton( + Icons.hd_rounded, + tooltipText: context.lang.toggleHighQuality, + color: useHighQuality + ? Colors.white + : Colors.white.withAlpha(160), + onPressed: () async { + useHighQuality = !useHighQuality; + setState(() {}); + var user = await getUser(); + if (user != null) { + user.useHighQuality = useHighQuality; + updateUser(user); + } + }, + ), + if (!hasAudioPermission) + ActionButton( + Icons.mic_off_rounded, + color: Colors.white.withAlpha(160), + tooltipText: + "Allow microphone access for video recording.", + onPressed: requestMicrophonePermission, + ), + if (hasAudioPermission) + ActionButton( + (videoWithAudio) + ? Icons.volume_up_rounded + : Icons.volume_off_rounded, + tooltipText: "Record video with audio.", + color: (videoWithAudio) + ? Colors.white + : Colors.white.withAlpha(160), + onPressed: () async { + setState(() { + videoWithAudio = !videoWithAudio; + }); + }, + ), + ], + ), + ), + ), + ), + if (!sharePreviewIsShown) + Positioned( + bottom: 30, + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + child: Column( + children: [ + if (HomeViewState.cameraController!.value.isInitialized && + HomeViewState.selectedCameraDetails.isZoomAble && + !isFront && + !isVideoRecording) + SizedBox( + width: 120, + child: CameraZoomButtons( + key: widget.key, + scaleFactor: + HomeViewState.selectedCameraDetails.scaleFactor, + updateScaleFactor: updateScaleFactor, + controller: HomeViewState.cameraController!, + ), + ), + const SizedBox(height: 30), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!isVideoRecording) + GestureDetector( + onTap: pickImageFromGallery, + child: Align( + alignment: Alignment.center, + child: Container( + height: 50, + width: 80, + padding: const EdgeInsets.all(2), + child: Center( + child: FaIcon( + FontAwesomeIcons.photoFilm, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ), + GestureDetector( + onTap: takePicture, + // onLongPress: startVideoRecording, + key: keyTriggerButton, + child: Align( + alignment: Alignment.center, + child: Container( + height: 100, + width: 100, + clipBehavior: Clip.antiAliasWithSaveLayer, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 7, + color: isVideoRecording + ? Colors.red + : Colors.white, + ), + ), + ), + ), + ), + if (!isVideoRecording) SizedBox(width: 80) + ], + ), + ], + ), + ), + ), + if (videoRecordingStarted != null) + Positioned( + top: 50, + left: 0, + right: 0, + child: Center( + child: SizedBox( + width: 50, + height: 50, + child: Stack( + children: [ + Center( + child: CircularProgressIndicator( + value: + (currentTime.difference(videoRecordingStarted!)) + .inMilliseconds / + (maxVideoRecordingTime * 1000), + strokeWidth: 4, + valueColor: + AlwaysStoppedAnimation(Colors.red), + backgroundColor: Colors.grey[300], + ), + ), + Center( + child: Text( + currentTime + .difference(videoRecordingStarted!) + .inSeconds + .toString(), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 17, + shadows: [ + Shadow( + color: const Color.fromARGB(122, 0, 0, 0), + blurRadius: 5.0, + ) + ], + ), + ), + ) + ], + ), + ), + ), + ), + if (!sharePreviewIsShown && widget.sendTo != null) + Positioned( + left: 5, + top: 10, + child: ActionButton( + FontAwesomeIcons.xmark, + tooltipText: context.lang.close, + onPressed: () async { + Navigator.pop(context); + }, + ), + ), + if (showSelfieFlash) + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Container( + color: Colors.white, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/views/camera/camera_preview_view.dart b/lib/src/views/camera/camera_preview_view.dart deleted file mode 100644 index aadc815..0000000 --- a/lib/src/views/camera/camera_preview_view.dart +++ /dev/null @@ -1,844 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:logging/logging.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:screenshot/screenshot.dart'; -import 'package:twonly/globals.dart'; -import 'package:twonly/src/views/camera/components/zoom_selector.dart'; -import 'package:twonly/src/database/daos/contacts_dao.dart'; -import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/views/camera/image_editor/action_button.dart'; -import 'package:twonly/src/views/components/media_view_sizing.dart'; -import 'package:twonly/src/views/components/permissions_view.dart'; -import 'package:twonly/src/utils/storage.dart'; -import 'package:twonly/src/views/camera/share_image_editor_view.dart'; -import 'package:twonly/src/views/home_view.dart'; - -int maxVideoRecordingTime = 15; - -class CameraPreviewView extends StatefulWidget { - const CameraPreviewView({super.key, this.sendTo}); - final Contact? sendTo; - - @override - State createState() => _CameraPreviewViewState(); -} - -class _CameraPreviewViewState extends State { - double scaleFactor = 1; - bool sharePreviewIsShown = false; - bool galleryLoadedImageIsShown = false; - bool isFlashOn = false; - bool showSelfieFlash = false; - int cameraId = 0; - bool isZoomAble = false; - double basePanY = 0; - double baseScaleFactor = 0; - bool cameraLoaded = false; - bool useHighQuality = false; - bool isVideoRecording = false; - bool hasAudioPermission = true; - bool videoWithAudio = true; - DateTime? videoRecordingStarted; - Timer? videoRecordingTimer; - - double _minAvailableZoom = 1.0; - double _maxAvailableZoom = 1.0; - - DateTime currentTime = DateTime.now(); - final GlobalKey keyTriggerButton = GlobalKey(); - final GlobalKey navigatorKey = GlobalKey(); - - CameraController? controller; - ScreenshotController screenshotController = ScreenshotController(); - - @override - void initState() { - super.initState(); - selectCamera(0, init: true); - initAsync(); - } - - void initAsync() async { - final user = await getUser(); - if (user == null) return; - if (user.useHighQuality != null) { - useHighQuality = user.useHighQuality!; - } - hasAudioPermission = await Permission.microphone.isGranted; - if (!mounted) return; - setState(() {}); - } - - @override - void dispose() { - if (cameraId < gCameras.length) { - controller?.dispose(); - } - videoRecordingTimer?.cancel(); - super.dispose(); - } - - Future requestMicrophonePermission() async { - Map statuses = await [ - Permission.microphone, - ].request(); - if (statuses[Permission.microphone]!.isPermanentlyDenied) { - openAppSettings(); - } else { - hasAudioPermission = await Permission.microphone.isGranted; - if (hasAudioPermission) { - selectCamera(cameraId); - } - } - } - - Future selectCamera( - int sCameraId, { - bool init = false, - bool enableAudio = false, - }) async { - if (sCameraId >= gCameras.length) return; - if (init) { - for (; sCameraId < gCameras.length; sCameraId++) { - if (gCameras[sCameraId].lensDirection == CameraLensDirection.back) { - break; - } - } - } - setState(() { - isZoomAble = false; - if (cameraId != sCameraId) { - // switch between front and back - scaleFactor = 1; - } - }); - controller = CameraController( - gCameras[sCameraId], - ResolutionPreset.high, - enableAudio: enableAudio, - ); - await controller?.initialize().then((_) async { - if (!mounted) { - return; - } - - await controller!.setZoomLevel(scaleFactor); - await controller?.lockCaptureOrientation(DeviceOrientation.portraitUp); - controller?.setFlashMode(isFlashOn ? FlashMode.always : FlashMode.off); - - controller - ?.getMaxZoomLevel() - .then((double value) => _maxAvailableZoom = value); - controller - ?.getMinZoomLevel() - .then((double value) => _minAvailableZoom = value); - isZoomAble = await controller?.getMinZoomLevel() != - await controller?.getMaxZoomLevel(); - setState(() { - cameraLoaded = true; - }); - }).catchError((Object e) { - if (e is CameraException) { - switch (e.code) { - case 'CameraAccessDenied': - // Handle access errors here. - break; - default: - // Handle other errors here. - break; - } - } - }); - if (!mounted) { - return; - } - setState(() { - cameraId = sCameraId; - }); - } - - Future updateScaleFactor(double newScale) async { - if (scaleFactor == newScale || controller == null) return; - await controller - ?.setZoomLevel(newScale.clamp(_minAvailableZoom, _maxAvailableZoom)); - setState(() { - scaleFactor = newScale; - }); - } - - Future loadAndDeletePictureFromFile(XFile picture) async { - try { - // Load the image into bytes - final Uint8List imageBytes = await picture.readAsBytes(); - // Remove the image file - await File(picture.path).delete(); - return imageBytes; - } catch (e) { - if (context.mounted) { - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error loading picture: $e'), - duration: Duration(seconds: 3), - ), - ); - } - return null; - } - } - - Future takePicture() async { - if (sharePreviewIsShown || isVideoRecording) return; - late Future imageBytes; - - setState(() { - sharePreviewIsShown = true; - }); - - // if (useHighQuality && !isFront) { - // if (Platform.isIOS) { - // await controller?.pausePreview(); - // if (!context.mounted) return; - // } - // try { - // // Take the picture - // final XFile? picture = await controller?.takePicture(); - // if (picture == null) return; - // imageBytes = loadAndDeletePictureFromFile(picture); - // } catch (e) { - // _showCameraException(e); - // return; - // } - // } else { - if (isFlashOn) { - if (isFront) { - setState(() { - showSelfieFlash = true; - }); - } else { - controller?.setFlashMode(FlashMode.torch); - } - await Future.delayed(Duration(milliseconds: 1000)); - } - - await controller?.pausePreview(); - if (!context.mounted) return; - - controller?.setFlashMode(isFlashOn ? FlashMode.always : FlashMode.off); - imageBytes = screenshotController.capture( - pixelRatio: - (useHighQuality) ? MediaQuery.of(context).devicePixelRatio : 1); - // } - - if (await pushMediaEditor(imageBytes, null)) { - return; - } - } - - Future pushMediaEditor( - Future? imageBytes, File? videoFilePath) async { - bool? shoudReturn = await Navigator.push( - context, - PageRouteBuilder( - opaque: false, - pageBuilder: (context, a1, a2) => ShareImageEditorView( - videoFilePath: videoFilePath, - imageBytes: imageBytes, - sendTo: widget.sendTo, - mirrorVideo: isFront && Platform.isAndroid, - useHighQuality: useHighQuality, - ), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return child; - }, - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - ), - ); - if (!context.mounted) return true; - // shouldReturn is null when the user used the back button - if (shoudReturn != null && shoudReturn) { - // ignore: use_build_context_synchronously - if (widget.sendTo == null) { - globalUpdateOfHomeViewPageIndex(0); - } else { - Navigator.pop(context); - } - return true; - } - selectCamera(cameraId); - if (context.mounted) { - setState(() { - sharePreviewIsShown = false; - showSelfieFlash = false; - }); - } - return false; - } - - bool get isFront => - controller?.description.lensDirection == CameraLensDirection.front; - - Future onPanUpdate(details) async { - if (isFront) { - return; - } - if (controller == null) return; - if (!controller!.value.isInitialized) return; - - scaleFactor = (baseScaleFactor + (basePanY - details.localPosition.dy) / 30) - .clamp(1, _maxAvailableZoom); - - await controller!.setZoomLevel(scaleFactor); - if (mounted) { - setState(() {}); - } - } - - Future pickImageFromGallery() async { - setState(() { - galleryLoadedImageIsShown = true; - sharePreviewIsShown = true; - }); - final picker = ImagePicker(); - final pickedFile = await picker.pickImage(source: ImageSource.gallery); - - if (pickedFile != null) { - File imageFile = File(pickedFile.path); - if (await pushMediaEditor(imageFile.readAsBytes(), null)) { - return; - } - } - setState(() { - galleryLoadedImageIsShown = false; - sharePreviewIsShown = false; - }); - } - - Future startVideoRecording() async { - if (controller != null && controller!.value.isRecordingVideo) return; - if (hasAudioPermission && videoWithAudio) { - await selectCamera(cameraId, - enableAudio: await Permission.microphone.isGranted && videoWithAudio); - } - - try { - await controller?.startVideoRecording(); - videoRecordingTimer = Timer.periodic(Duration(milliseconds: 15), (timer) { - setState(() { - currentTime = DateTime.now(); - }); - - if (videoRecordingStarted != null && - currentTime.difference(videoRecordingStarted!).inSeconds >= - maxVideoRecordingTime) { - timer.cancel(); - videoRecordingTimer = null; - stopVideoRecording(); - } - }); - setState(() { - videoRecordingStarted = DateTime.now(); - isVideoRecording = true; - }); - } on CameraException catch (e) { - _showCameraException(e); - return; - } - } - - Future stopVideoRecording() async { - if (videoRecordingTimer != null) { - videoRecordingTimer?.cancel(); - videoRecordingTimer = null; - } - if (controller == null || !controller!.value.isRecordingVideo) { - return null; - } - - try { - setState(() { - videoRecordingStarted = null; - isVideoRecording = false; - sharePreviewIsShown = true; - }); - File? videoPathFile; - XFile? videoPath = await controller?.stopVideoRecording(); - if (videoPath != null) { - if (Platform.isAndroid) { - // see https://github.com/flutter/flutter/issues/148335 - await File(videoPath.path).rename("${videoPath.path}.mp4"); - videoPathFile = File("${videoPath.path}.mp4"); - } else { - videoPathFile = File(videoPath.path); - } - } - await controller?.pausePreview(); - if (await pushMediaEditor(null, videoPathFile)) { - return; - } - } on CameraException catch (e) { - _showCameraException(e); - return null; - } - } - - void _showCameraException(dynamic e) { - Logger("ui.camera").shout("$e"); - try { - if (context.mounted) { - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error: $e'), - duration: Duration(seconds: 3), - ), - ); - } - // ignore: empty_catches - } catch (e) {} - } - - @override - Widget build(BuildContext context) { - if (cameraId >= gCameras.length || controller == null) { - return Container(); - } - return MediaViewSizing( - child: GestureDetector( - onPanStart: (details) async { - if (isFront) { - return; - } - setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = scaleFactor; - }); - }, - onDoubleTap: () async { - selectCamera((cameraId + 1) % 2); - }, - onLongPressMoveUpdate: onPanUpdate, - onLongPressStart: (details) { - setState(() { - basePanY = details.localPosition.dy; - baseScaleFactor = scaleFactor; - }); - // Get the position of the pointer - RenderBox renderBox = - keyTriggerButton.currentContext?.findRenderObject() as RenderBox; - Offset localPosition = - renderBox.globalToLocal(details.globalPosition); - - final containerRect = - Rect.fromLTWH(0, 0, renderBox.size.width, renderBox.size.height); - - if (containerRect.contains(localPosition)) { - startVideoRecording(); - } - }, - onLongPressEnd: (a) { - stopVideoRecording(); - }, - onPanEnd: (a) { - stopVideoRecording(); - }, - onPanUpdate: onPanUpdate, - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(22), - child: Stack( - children: [ - if (!galleryLoadedImageIsShown) - CameraPreviewWidget( - controller: controller!, - screenshotController: screenshotController, - isFront: isFront, - ), - if (galleryLoadedImageIsShown) - Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 1, color: context.color.primary), - ), - ), - // Positioned.fill( - // child: GestureDetector(), - // ), - if (!sharePreviewIsShown && - widget.sendTo != null && - !isVideoRecording) - SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), - if (!sharePreviewIsShown && !isVideoRecording) - Positioned( - right: 5, - top: 0, - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.symmetric(vertical: 16), - child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ActionButton( - Icons.repeat_rounded, - tooltipText: - context.lang.switchFrontAndBackCamera, - onPressed: () async { - selectCamera((cameraId + 1) % 2); - }, - ), - ActionButton( - isFlashOn - ? Icons.flash_on_rounded - : Icons.flash_off_rounded, - tooltipText: context.lang.toggleFlashLight, - color: isFlashOn - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - if (isFlashOn) { - controller?.setFlashMode(FlashMode.off); - isFlashOn = false; - } else { - controller?.setFlashMode(FlashMode.always); - isFlashOn = true; - } - setState(() {}); - }, - ), - if (!isFront) - ActionButton( - Icons.hd_rounded, - tooltipText: context.lang.toggleHighQuality, - color: useHighQuality - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - useHighQuality = !useHighQuality; - setState(() {}); - var user = await getUser(); - if (user != null) { - user.useHighQuality = useHighQuality; - updateUser(user); - } - }, - ), - if (!hasAudioPermission) - ActionButton( - Icons.mic_off_rounded, - color: Colors.white.withAlpha(160), - tooltipText: - "Allow microphone access for video recording.", - onPressed: requestMicrophonePermission, - ), - if (hasAudioPermission) - ActionButton( - (videoWithAudio) - ? Icons.volume_up_rounded - : Icons.volume_off_rounded, - tooltipText: "Record video with audio.", - color: (videoWithAudio) - ? Colors.white - : Colors.white.withAlpha(160), - onPressed: () async { - setState(() { - videoWithAudio = !videoWithAudio; - }); - selectCamera(cameraId); - }, - ), - ], - ), - ), - ), - ), - if (!sharePreviewIsShown) - Positioned( - bottom: 30, - left: 0, - right: 0, - child: Align( - alignment: Alignment.bottomCenter, - child: Column( - children: [ - if (controller!.value.isInitialized && - isZoomAble && - !isFront && - !isVideoRecording) - SizedBox( - width: 120, - child: CameraZoomButtons( - key: widget.key, - scaleFactor: scaleFactor, - updateScaleFactor: updateScaleFactor, - controller: controller!, - ), - ), - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!isVideoRecording) - GestureDetector( - onTap: pickImageFromGallery, - child: Align( - alignment: Alignment.center, - child: Container( - height: 50, - width: 80, - padding: const EdgeInsets.all(2), - child: Center( - child: FaIcon( - FontAwesomeIcons.photoFilm, - color: Colors.white, - size: 25, - ), - ), - ), - ), - ), - GestureDetector( - onTap: takePicture, - // onLongPress: startVideoRecording, - key: keyTriggerButton, - child: Align( - alignment: Alignment.center, - child: Container( - height: 100, - width: 100, - clipBehavior: Clip.antiAliasWithSaveLayer, - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 7, - color: isVideoRecording - ? Colors.red - : Colors.white, - ), - ), - ), - ), - ), - if (!isVideoRecording) SizedBox(width: 80) - ], - ), - ], - ), - ), - ), - ], - ), - ), - if (videoRecordingStarted != null) - Positioned( - top: 50, - left: 0, - right: 0, - child: Center( - child: SizedBox( - width: 50, - height: 50, - child: Stack( - children: [ - Center( - child: CircularProgressIndicator( - value: - (currentTime.difference(videoRecordingStarted!)) - .inMilliseconds / - (maxVideoRecordingTime * 1000), - strokeWidth: 4, - valueColor: - AlwaysStoppedAnimation(Colors.red), - backgroundColor: Colors.grey[300], - ), - ), - Center( - child: Text( - currentTime - .difference(videoRecordingStarted!) - .inSeconds - .toString(), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 17, - shadows: [ - Shadow( - color: const Color.fromARGB(122, 0, 0, 0), - blurRadius: 5.0, - ) - ], - ), - ), - ) - ], - ), - ), - ), - ), - if (!sharePreviewIsShown && widget.sendTo != null) - Positioned( - left: 5, - top: 10, - child: ActionButton( - FontAwesomeIcons.xmark, - tooltipText: context.lang.close, - onPressed: () async { - Navigator.pop(context); - }, - ), - ), - if (showSelfieFlash) - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(22), - child: Container( - color: Colors.white, - ), - ), - ), - ], - ), - ), - ); - } -} - -class SendToWidget extends StatelessWidget { - final String sendTo; - - const SendToWidget({ - super.key, - required this.sendTo, - }); - - @override - Widget build(BuildContext context) { - TextStyle textStyle = TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 24, - decoration: TextDecoration.none, - shadows: [ - Shadow( - color: const Color.fromARGB(122, 0, 0, 0), - blurRadius: 5.0, - ), - ], - ); - - TextStyle boldTextStyle = textStyle.copyWith( - fontWeight: FontWeight.normal, - fontSize: 28, - ); - - return Positioned( - right: 0, - left: 0, - top: 50, - child: Column( - children: [ - Text( - context.lang.cameraPreviewSendTo, - textAlign: TextAlign.center, - style: textStyle, - ), - Text( - sendTo, - textAlign: TextAlign.center, - style: boldTextStyle, // Use the bold text style here - ), - ], - ), - ); - } - - String getContactDisplayName(String contact) { - // Replace this with your actual logic to get the contact display name - return contact; // Placeholder implementation - } -} - -class CameraPreviewWidget extends StatelessWidget { - final CameraController controller; - final ScreenshotController screenshotController; - final bool isFront; - - const CameraPreviewWidget({ - super.key, - required this.controller, - required this.screenshotController, - required this.isFront, - }); - - @override - Widget build(BuildContext context) { - return (controller.value.isInitialized) - ? Positioned.fill( - child: Screenshot( - controller: screenshotController, - child: AspectRatio( - aspectRatio: 9 / 16, - child: ClipRect( - child: FittedBox( - fit: BoxFit.cover, - child: SizedBox( - width: controller.value.previewSize!.height, - height: controller.value.previewSize!.width, - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY( - (isFront && Platform.isAndroid) ? 3.14 : 0), - child: CameraPreview(controller), - ), - ), - ), - ), - ), - ), - ) - : Container(); - } -} - -class CameraPreviewViewPermission extends StatefulWidget { - const CameraPreviewViewPermission({super.key, this.sendTo}); - final Contact? sendTo; - - @override - State createState() => - _CameraPreviewViewPermission(); -} - -class _CameraPreviewViewPermission extends State { - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: checkPermissions(), - builder: (context, snap) { - if (snap.hasData) { - if (snap.data!) { - return CameraPreviewView(sendTo: widget.sendTo); - } else { - return PermissionHandlerView(onSuccess: () { - setState(() {}); - }); - } - } else { - return Container(); - } - }); - } -} diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index 00a3d4f..822be96 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:twonly/src/database/twonly_database.dart'; -import 'package:twonly/src/views/camera/camera_preview_view.dart'; +import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; class CameraSendToView extends StatefulWidget { const CameraSendToView(this.sendTo, {super.key}); @@ -12,8 +12,9 @@ class CameraSendToView extends StatefulWidget { class CameraSendToViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: CameraPreviewViewPermission(sendTo: widget.sendTo), - ); + return Scaffold(); + // return Scaffold( + // body: CameraPreviewControllerView(sendTo: widget.sendTo), + // ); } } diff --git a/lib/src/views/camera/components/best_friends_selector.dart b/lib/src/views/camera/share_image_components/best_friends_selector.dart similarity index 100% rename from lib/src/views/camera/components/best_friends_selector.dart rename to lib/src/views/camera/share_image_components/best_friends_selector.dart diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 4990f77..34ba504 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -7,7 +7,7 @@ import 'package:logging/logging.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart' show ErrorCode; import 'package:twonly/src/providers/api/media_send.dart'; -import 'package:twonly/src/views/camera/components/save_to_gallery.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/save_to_gallery.dart'; import 'package:twonly/src/views/camera/image_editor/action_button.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/media_view_sizing.dart'; diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index e099ecc..ea4c79f 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -6,7 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/providers/api/media_send.dart'; -import 'package:twonly/src/views/camera/components/best_friends_selector.dart'; +import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/headline.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; diff --git a/lib/src/views/gallery/gallery_main_view.dart b/lib/src/views/gallery/gallery_main_view.dart index b5b315c..1f6bc76 100644 --- a/lib/src/views/gallery/gallery_main_view.dart +++ b/lib/src/views/gallery/gallery_main_view.dart @@ -128,6 +128,7 @@ class GalleryMainViewState extends State { List galleryItems = []; Map> orderedByMonth = {}; List months = []; + bool mounted = true; @override void initState() { @@ -135,6 +136,12 @@ class GalleryMainViewState extends State { initAsync(); } + @override + void dispose() { + mounted = false; + super.dispose(); + } + Future> loadMemoriesDirectory() async { final directoryPath = await send.getMediaBaseFilePath("memories"); final directory = Directory(directoryPath); @@ -219,7 +226,9 @@ class GalleryMainViewState extends State { } orderedByMonth.putIfAbsent(month, () => []).add(i); } - setState(() {}); + if (mounted) { + setState(() {}); + } } @override diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index f8d9038..22a278b 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,10 +1,19 @@ +import 'dart:async'; + +import 'package:camera/camera.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:logging/logging.dart'; import 'package:pie_menu/pie_menu.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.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/components/user_context_menu.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/views/gallery/gallery_main_view.dart'; -import 'camera/camera_preview_view.dart'; +import 'camera/camera_preview_controller_view.dart'; import 'chats/chat_list_view.dart'; import 'package:flutter/material.dart'; @@ -21,29 +30,133 @@ class HomeView extends StatefulWidget { State createState() => HomeViewState(); } +class Shade extends StatelessWidget { + const Shade({super.key, required this.opacity}); + final double opacity; + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Opacity( + opacity: opacity, + child: Container( + color: context.color.surface, + ), + ), + ); + } +} + class HomeViewState extends State { int activePageIdx = 0; - late PageController homeViewPageController; + + final PageController homeViewPageController = + PageController(keepPage: true, initialPage: 1); + + double buttonDiameter = 100.0; + double offsetRatio = 0.0; + double offsetFromOne = 0.0; + + Timer? disableCameraTimer; + + static CameraController? cameraController; + static ScreenshotController screenshotController = ScreenshotController(); + static SelectedCameraDetails selectedCameraDetails = SelectedCameraDetails(); + + bool onPageView(ScrollNotification notification) { + disableCameraTimer?.cancel(); + if (notification.depth == 0 && notification is ScrollUpdateNotification) { + setState(() { + offsetFromOne = 1.0 - (homeViewPageController.page ?? 0); + offsetRatio = offsetFromOne.abs(); + }); + } + if (cameraController == null) { + selectCamera(selectedCameraDetails.cameraId, false, false); + } + if (offsetRatio == 1) { + disableCameraTimer = Timer(Duration(seconds: 2), () { + cameraController?.dispose(); + cameraController = null; + disableCameraTimer = null; + }); + } + return false; + } @override void initState() { super.initState(); activePageIdx = widget.initialPage; - homeViewPageController = PageController(initialPage: widget.initialPage); globalUpdateOfHomeViewPageIndex = (index) { homeViewPageController.jumpToPage(index); setState(() { activePageIdx = index; }); }; - selectNotificationStream.stream .listen((NotificationResponse? response) async { globalUpdateOfHomeViewPageIndex(0); }); + selectCamera(0, true, false); initAsync(); } + @override + void dispose() { + selectNotificationStream.close(); + disableCameraTimer?.cancel(); + super.dispose(); + } + + Future selectCamera(int sCameraId, bool init, bool enableAudio) async { + if (sCameraId >= gCameras.length) return; + if (init) { + for (; sCameraId < gCameras.length; sCameraId++) { + if (gCameras[sCameraId].lensDirection == CameraLensDirection.back) { + break; + } + } + } + selectedCameraDetails.isZoomAble = false; + if (selectedCameraDetails.cameraId != sCameraId) { + // switch between front and back + selectedCameraDetails.scaleFactor = 1; + } + + cameraController = CameraController( + gCameras[sCameraId], + ResolutionPreset.high, + enableAudio: enableAudio, + ); + + await cameraController?.initialize().then((_) async { + await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor); + await cameraController + ?.lockCaptureOrientation(DeviceOrientation.portraitUp); + cameraController?.setFlashMode( + selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off); + await cameraController?.getMaxZoomLevel().then( + (double value) => selectedCameraDetails.maxAvailableZoom = value); + await cameraController?.getMinZoomLevel().then( + (double value) => selectedCameraDetails.minAvailableZoom = value); + selectedCameraDetails.isZoomAble = + selectedCameraDetails.maxAvailableZoom != + selectedCameraDetails.minAvailableZoom; + setState(() { + selectedCameraDetails.cameraLoaded = true; + selectedCameraDetails.cameraId = sCameraId; + }); + }).catchError((Object e) { + Logger("home_view.dart").shout("$e"); + }); + setState(() {}); + } + + Future toggleSelectedCamera() async { + selectCamera((selectedCameraDetails.cameraId + 1) % 2, false, false); + } + Future initAsync() async { var notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); @@ -55,28 +168,52 @@ class HomeViewState extends State { } } - @override - void dispose() { - selectNotificationStream.close(); - super.dispose(); - } - @override Widget build(BuildContext context) { return PieCanvas( theme: getPieCanvasTheme(context), child: Scaffold( - body: PageView( - controller: homeViewPageController, - onPageChanged: (index) { - activePageIdx = index; - setState(() {}); - }, - children: [ - ChatListView(), - CameraPreviewViewPermission(), - GalleryMainView() - ], + body: GestureDetector( + onDoubleTap: offsetRatio == 0 ? toggleSelectedCamera : null, + child: Stack( + children: [ + CameraPreviewWidget(), + Shade( + opacity: offsetRatio, + ), + NotificationListener( + onNotification: onPageView, + child: Positioned.fill( + child: PageView( + controller: homeViewPageController, + onPageChanged: (index) { + setState(() { + activePageIdx = index; + }); + }, + children: [ + ChatListView(), + Container(), + GalleryMainView(), + ], + ), + ), + ), + Positioned( + left: 0, + top: 0, + right: 0, + bottom: (offsetRatio > 0.25) + ? MediaQuery.sizeOf(context).height * 2 + : 0, + child: Opacity( + opacity: (1 - (offsetRatio * 4) % 1), + child: CameraPreviewControllerView( + selectCamera: selectCamera, + ), + )), + ], + ), ), bottomNavigationBar: BottomNavigationBar( showSelectedLabels: false, @@ -88,7 +225,9 @@ class HomeViewState extends State { color: Theme.of(context).colorScheme.inverseSurface), items: [ BottomNavigationBarItem( - icon: FaIcon(FontAwesomeIcons.solidComments), label: ""), + icon: FaIcon(FontAwesomeIcons.solidComments), + label: "", + ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.camera), label: "",