mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +00:00
816 lines
26 KiB
Dart
816 lines
26 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:camera/camera.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart';
|
|
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:screenshot/screenshot.dart';
|
|
import 'package:twonly/globals.dart';
|
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
|
import 'package:twonly/src/database/twonly.db.dart';
|
|
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
|
import 'package:twonly/src/utils/log.dart';
|
|
import 'package:twonly/src/utils/misc.dart';
|
|
import 'package:twonly/src/utils/storage.dart';
|
|
import 'package:twonly/src/views/camera/camera_preview_components/permissions_view.dart';
|
|
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/zoom_selector.dart';
|
|
import 'package:twonly/src/views/camera/image_editor/action_button.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/home.view.dart';
|
|
|
|
int maxVideoRecordingTime = 60;
|
|
|
|
Future<(SelectedCameraDetails, CameraController)?> initializeCameraController(
|
|
SelectedCameraDetails details,
|
|
int sCameraId,
|
|
bool init,
|
|
) async {
|
|
var cameraId = sCameraId;
|
|
if (cameraId >= gCameras.length) return null;
|
|
if (init) {
|
|
for (; cameraId < gCameras.length; cameraId++) {
|
|
if (gCameras[cameraId].lensDirection == CameraLensDirection.back) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
details.isZoomAble = false;
|
|
if (details.cameraId != cameraId) {
|
|
// switch between front and back
|
|
details.scaleFactor = 1;
|
|
}
|
|
|
|
final cameraController = CameraController(
|
|
gCameras[cameraId],
|
|
ResolutionPreset.high,
|
|
enableAudio: await Permission.microphone.isGranted,
|
|
);
|
|
|
|
await cameraController.initialize().then((_) async {
|
|
await cameraController.setZoomLevel(details.scaleFactor);
|
|
await cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp);
|
|
await cameraController
|
|
.setFlashMode(details.isFlashOn ? FlashMode.always : FlashMode.off);
|
|
await cameraController
|
|
.getMaxZoomLevel()
|
|
.then((double value) => details.maxAvailableZoom = value);
|
|
await cameraController
|
|
.getMinZoomLevel()
|
|
.then((double value) => details.minAvailableZoom = value);
|
|
details
|
|
..isZoomAble = details.maxAvailableZoom != details.minAvailableZoom
|
|
..cameraLoaded = true
|
|
..cameraId = cameraId;
|
|
}).catchError((Object e) {
|
|
Log.error('$e');
|
|
});
|
|
return (details, cameraController);
|
|
}
|
|
|
|
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 StatelessWidget {
|
|
const CameraPreviewControllerView({
|
|
required this.cameraController,
|
|
required this.selectCamera,
|
|
required this.selectedCameraDetails,
|
|
required this.screenshotController,
|
|
required this.isVisible,
|
|
super.key,
|
|
this.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;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder(
|
|
future: checkPermissions(),
|
|
builder: (context, snap) {
|
|
if (snap.hasData) {
|
|
if (snap.data!) {
|
|
return CameraPreviewView(
|
|
sendToGroup: sendToGroup,
|
|
selectCamera: selectCamera,
|
|
cameraController: cameraController,
|
|
selectedCameraDetails: selectedCameraDetails,
|
|
screenshotController: screenshotController,
|
|
isVisible: isVisible,
|
|
);
|
|
} else {
|
|
return PermissionHandlerView(
|
|
onSuccess: () {
|
|
selectCamera(0, true);
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
return Container();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class CameraPreviewView extends StatefulWidget {
|
|
const CameraPreviewView({
|
|
required this.selectCamera,
|
|
required this.cameraController,
|
|
required this.selectedCameraDetails,
|
|
required this.screenshotController,
|
|
required this.isVisible,
|
|
super.key,
|
|
this.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;
|
|
|
|
@override
|
|
State<CameraPreviewView> createState() => _CameraPreviewViewState();
|
|
}
|
|
|
|
class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|
bool _sharePreviewIsShown = false;
|
|
bool _galleryLoadedImageIsShown = false;
|
|
bool _showSelfieFlash = false;
|
|
double _basePanY = 0;
|
|
double _baseScaleFactor = 0;
|
|
bool _isVideoRecording = false;
|
|
bool _hasAudioPermission = true;
|
|
DateTime? _videoRecordingStarted;
|
|
Timer? _videoRecordingTimer;
|
|
|
|
DateTime _currentTime = DateTime.now();
|
|
final GlobalKey keyTriggerButton = GlobalKey();
|
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
|
|
StreamSubscription<HardwareButton>? androidVolumeDownSub;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initVolumeControl();
|
|
initAsync();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant CameraPreviewView oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.isVisible != widget.isVisible) {
|
|
if (widget.isVisible) {
|
|
initVolumeControl();
|
|
} else {
|
|
deInitVolumeControl();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_videoRecordingTimer?.cancel();
|
|
deInitVolumeControl();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> initVolumeControl() async {
|
|
if (Platform.isIOS) {
|
|
await FlutterVolumeController.updateShowSystemUI(false);
|
|
double? startedVolume;
|
|
|
|
FlutterVolumeController.addListener(
|
|
(volume) async {
|
|
if (!widget.isVisible) {
|
|
await deInitVolumeControl();
|
|
return;
|
|
}
|
|
if (startedVolume == null) {
|
|
startedVolume = volume;
|
|
return;
|
|
}
|
|
if (startedVolume == volume) {
|
|
return;
|
|
}
|
|
// reset the volume back to the original value
|
|
await FlutterVolumeController.setVolume(startedVolume!);
|
|
await takePicture();
|
|
},
|
|
);
|
|
}
|
|
if (Platform.isAndroid) {
|
|
if ((await DeviceInfoPlugin().androidInfo).version.release == '9') {
|
|
// MissingPluginException: MissingPluginException(No implementation found for method cancel on channel dart-tools.dev/flutter_…
|
|
// Maybe this is the reason?
|
|
return;
|
|
} else {
|
|
androidVolumeDownSub =
|
|
FlutterAndroidVolumeKeydown.stream.listen((event) {
|
|
if (widget.isVisible) {
|
|
takePicture();
|
|
} else {
|
|
deInitVolumeControl();
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> deInitVolumeControl() async {
|
|
if (Platform.isIOS) {
|
|
await FlutterVolumeController.updateShowSystemUI(true);
|
|
FlutterVolumeController.removeListener();
|
|
}
|
|
if (Platform.isAndroid) {
|
|
await androidVolumeDownSub?.cancel();
|
|
}
|
|
}
|
|
|
|
Future<void> initAsync() async {
|
|
_hasAudioPermission = await Permission.microphone.isGranted;
|
|
|
|
if (!_hasAudioPermission && !gUser.requestedAudioPermission) {
|
|
await updateUserdata((u) {
|
|
u.requestedAudioPermission = true;
|
|
return u;
|
|
});
|
|
await requestMicrophonePermission();
|
|
}
|
|
if (!mounted) return;
|
|
setState(() {});
|
|
}
|
|
|
|
Future<void> requestMicrophonePermission() async {
|
|
final statuses = await [
|
|
Permission.microphone,
|
|
].request();
|
|
if (statuses[Permission.microphone]!.isPermanentlyDenied) {
|
|
await openAppSettings();
|
|
} else {
|
|
_hasAudioPermission = await Permission.microphone.isGranted;
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Future<void> updateScaleFactor(double newScale) async {
|
|
if (widget.selectedCameraDetails.scaleFactor == newScale ||
|
|
widget.cameraController == null) {
|
|
return;
|
|
}
|
|
await widget.cameraController?.setZoomLevel(
|
|
newScale.clamp(
|
|
widget.selectedCameraDetails.minAvailableZoom,
|
|
widget.selectedCameraDetails.maxAvailableZoom,
|
|
),
|
|
);
|
|
setState(() {
|
|
widget.selectedCameraDetails.scaleFactor = newScale;
|
|
});
|
|
}
|
|
|
|
Future<Uint8List?> loadAndDeletePictureFromFile(XFile picture) async {
|
|
try {
|
|
// Load the image into bytes
|
|
final 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: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> takePicture() async {
|
|
if (_sharePreviewIsShown || _isVideoRecording) return;
|
|
late Future<Uint8List?> imageBytes;
|
|
|
|
setState(() {
|
|
_sharePreviewIsShown = true;
|
|
});
|
|
if (widget.selectedCameraDetails.isFlashOn) {
|
|
if (isFront) {
|
|
setState(() {
|
|
_showSelfieFlash = true;
|
|
});
|
|
} else {
|
|
await widget.cameraController?.setFlashMode(FlashMode.torch);
|
|
}
|
|
await Future.delayed(const Duration(milliseconds: 1000));
|
|
}
|
|
|
|
await widget.cameraController?.pausePreview();
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
if (Platform.isIOS) {
|
|
// android has a problem with this. Flash is turned off in the pausePreview function.
|
|
await widget.cameraController?.setFlashMode(FlashMode.off);
|
|
}
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
imageBytes = widget.screenshotController
|
|
.capture(pixelRatio: MediaQuery.of(context).devicePixelRatio);
|
|
|
|
if (await pushMediaEditor(imageBytes, null)) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_sharePreviewIsShown = false;
|
|
});
|
|
}
|
|
|
|
Future<bool> pushMediaEditor(
|
|
Future<Uint8List?>? imageBytes,
|
|
File? videoFilePath, {
|
|
bool sharedFromGallery = false,
|
|
MediaType? mediaType,
|
|
}) async {
|
|
final type = mediaType ??
|
|
((videoFilePath != null) ? MediaType.video : MediaType.image);
|
|
final mediaFileService = await initializeMediaUpload(
|
|
type,
|
|
gUser.defaultShowTime,
|
|
isDraftMedia: true,
|
|
);
|
|
if (!mounted) return true;
|
|
|
|
if (mediaFileService == null) {
|
|
Log.error('Could not generate media file service');
|
|
return false;
|
|
}
|
|
|
|
if (videoFilePath != null) {
|
|
videoFilePath
|
|
..copySync(mediaFileService.originalPath.path)
|
|
..deleteSync();
|
|
|
|
// Start with compressing the video, to speed up the process in case the video is not changed.
|
|
// unawaited(mediaFileService.compressMedia());
|
|
}
|
|
|
|
await deInitVolumeControl();
|
|
if (!mounted) return true;
|
|
|
|
final shouldReturn = await Navigator.push(
|
|
context,
|
|
PageRouteBuilder(
|
|
opaque: false,
|
|
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
|
imageBytesFuture: imageBytes,
|
|
sharedFromGallery: sharedFromGallery,
|
|
sendToGroup: widget.sendToGroup,
|
|
mediaFileService: mediaFileService,
|
|
),
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
return child;
|
|
},
|
|
transitionDuration: Duration.zero,
|
|
reverseTransitionDuration: Duration.zero,
|
|
),
|
|
) as bool?;
|
|
if (mounted) {
|
|
setState(() {
|
|
_sharePreviewIsShown = false;
|
|
_showSelfieFlash = false;
|
|
});
|
|
}
|
|
if (!mounted) return true;
|
|
await initVolumeControl();
|
|
// shouldReturn is null when the user used the back button
|
|
if (shouldReturn != null && shouldReturn) {
|
|
if (widget.sendToGroup == null) {
|
|
globalUpdateOfHomeViewPageIndex(0);
|
|
} else if (mounted) {
|
|
Navigator.pop(context);
|
|
}
|
|
return true;
|
|
}
|
|
await widget.selectCamera(
|
|
widget.selectedCameraDetails.cameraId,
|
|
false,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
bool get isFront =>
|
|
widget.cameraController?.description.lensDirection ==
|
|
CameraLensDirection.front;
|
|
|
|
Future<void> onPanUpdate(dynamic details) async {
|
|
if (isFront || details == null) {
|
|
return;
|
|
}
|
|
if (widget.cameraController == null ||
|
|
!widget.cameraController!.value.isInitialized) {
|
|
return;
|
|
}
|
|
|
|
widget.selectedCameraDetails.scaleFactor = (_baseScaleFactor +
|
|
// ignore: avoid_dynamic_calls
|
|
(_basePanY - (details.localPosition.dy as double)) / 30)
|
|
.clamp(1, widget.selectedCameraDetails.maxAvailableZoom);
|
|
|
|
await widget.cameraController!
|
|
.setZoomLevel(widget.selectedCameraDetails.scaleFactor);
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Future<void> pickImageFromGallery() async {
|
|
setState(() {
|
|
_galleryLoadedImageIsShown = true;
|
|
_sharePreviewIsShown = true;
|
|
});
|
|
final picker = ImagePicker();
|
|
final pickedFile = await picker.pickMedia();
|
|
|
|
if (pickedFile != null) {
|
|
final imageExtensions = [
|
|
'.png',
|
|
'.jpg',
|
|
'.jpeg',
|
|
'.gif',
|
|
'.webp',
|
|
'.heic',
|
|
'.heif',
|
|
'.avif',
|
|
];
|
|
|
|
Log.info('Picket from gallery: ${pickedFile.path}');
|
|
|
|
File? videoFilePath;
|
|
Future<Uint8List>? imageBytes;
|
|
MediaType? mediaType;
|
|
|
|
final isImage =
|
|
imageExtensions.any((ext) => pickedFile.name.contains(ext));
|
|
if (isImage) {
|
|
if (pickedFile.name.contains('.gif')) {
|
|
mediaType = MediaType.gif;
|
|
}
|
|
imageBytes = pickedFile.readAsBytes();
|
|
} else {
|
|
videoFilePath = File(pickedFile.path);
|
|
}
|
|
|
|
await pushMediaEditor(
|
|
imageBytes,
|
|
videoFilePath,
|
|
sharedFromGallery: true,
|
|
mediaType: mediaType,
|
|
);
|
|
}
|
|
setState(() {
|
|
_galleryLoadedImageIsShown = false;
|
|
_sharePreviewIsShown = false;
|
|
});
|
|
}
|
|
|
|
Future<void> startVideoRecording() async {
|
|
if (widget.cameraController != null &&
|
|
widget.cameraController!.value.isRecordingVideo) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_isVideoRecording = true;
|
|
});
|
|
|
|
try {
|
|
await widget.cameraController?.startVideoRecording();
|
|
_videoRecordingTimer =
|
|
Timer.periodic(const 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<void> stopVideoRecording() async {
|
|
if (_videoRecordingTimer != null) {
|
|
_videoRecordingTimer?.cancel();
|
|
_videoRecordingTimer = null;
|
|
}
|
|
|
|
setState(() {
|
|
_videoRecordingStarted = null;
|
|
_isVideoRecording = false;
|
|
});
|
|
|
|
if (widget.cameraController == null ||
|
|
!widget.cameraController!.value.isRecordingVideo) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_sharePreviewIsShown = true;
|
|
});
|
|
|
|
try {
|
|
final videoPath = await widget.cameraController?.stopVideoRecording();
|
|
if (videoPath == null) return;
|
|
await widget.cameraController?.pausePreview();
|
|
if (await pushMediaEditor(null, File(videoPath.path))) {
|
|
return;
|
|
}
|
|
} on CameraException catch (e) {
|
|
_showCameraException(e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void _showCameraException(dynamic e) {
|
|
Log.error('$e');
|
|
try {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: $e'),
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
// ignore: empty_catches
|
|
} catch (e) {}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.selectedCameraDetails.cameraId >= gCameras.length ||
|
|
widget.cameraController == null) {
|
|
return Container();
|
|
}
|
|
return MediaViewSizing(
|
|
requiredHeight: 80,
|
|
bottomNavigation: Container(),
|
|
child: GestureDetector(
|
|
onPanStart: (details) async {
|
|
if (isFront) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_basePanY = details.localPosition.dy;
|
|
_baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
|
|
});
|
|
},
|
|
onLongPressMoveUpdate: onPanUpdate,
|
|
onLongPressStart: (details) {
|
|
setState(() {
|
|
_basePanY = details.localPosition.dy;
|
|
_baseScaleFactor = widget.selectedCameraDetails.scaleFactor;
|
|
});
|
|
// Get the position of the pointer
|
|
final renderBox =
|
|
keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
|
|
final 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)
|
|
Center(
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 1,
|
|
color: context.color.primary,
|
|
),
|
|
),
|
|
),
|
|
if (!_sharePreviewIsShown &&
|
|
widget.sendToGroup != null &&
|
|
!_isVideoRecording)
|
|
SendToWidget(sendTo: widget.sendToGroup!.groupName),
|
|
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: <Widget>[
|
|
ActionButton(
|
|
Icons.repeat_rounded,
|
|
tooltipText: context.lang.switchFrontAndBackCamera,
|
|
onPressed: () async {
|
|
await widget.selectCamera(
|
|
(widget.selectedCameraDetails.cameraId + 1) % 2,
|
|
false,
|
|
);
|
|
},
|
|
),
|
|
ActionButton(
|
|
widget.selectedCameraDetails.isFlashOn
|
|
? Icons.flash_on_rounded
|
|
: Icons.flash_off_rounded,
|
|
tooltipText: context.lang.toggleFlashLight,
|
|
color: widget.selectedCameraDetails.isFlashOn
|
|
? Colors.white
|
|
: Colors.white.withAlpha(160),
|
|
onPressed: () async {
|
|
if (widget.selectedCameraDetails.isFlashOn) {
|
|
await widget.cameraController
|
|
?.setFlashMode(FlashMode.off);
|
|
widget.selectedCameraDetails.isFlashOn = false;
|
|
} else {
|
|
await widget.cameraController
|
|
?.setFlashMode(FlashMode.always);
|
|
widget.selectedCameraDetails.isFlashOn = true;
|
|
}
|
|
setState(() {});
|
|
},
|
|
),
|
|
if (!_hasAudioPermission)
|
|
ActionButton(
|
|
Icons.mic_off_rounded,
|
|
color: Colors.white.withAlpha(160),
|
|
tooltipText:
|
|
'Allow microphone access for video recording.',
|
|
onPressed: requestMicrophonePermission,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (!_sharePreviewIsShown)
|
|
Positioned(
|
|
bottom: 30,
|
|
left: 0,
|
|
right: 0,
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Column(
|
|
children: [
|
|
if (widget.cameraController!.value.isInitialized &&
|
|
widget.selectedCameraDetails.isZoomAble &&
|
|
!isFront &&
|
|
!_isVideoRecording)
|
|
SizedBox(
|
|
width: 120,
|
|
child: CameraZoomButtons(
|
|
key: widget.key,
|
|
scaleFactor:
|
|
widget.selectedCameraDetails.scaleFactor,
|
|
updateScaleFactor: updateScaleFactor,
|
|
selectCamera: widget.selectCamera,
|
|
selectedCameraDetails: widget.selectedCameraDetails,
|
|
controller: widget.cameraController!,
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (!_isVideoRecording)
|
|
GestureDetector(
|
|
onTap: pickImageFromGallery,
|
|
child: Align(
|
|
child: Container(
|
|
height: 50,
|
|
width: 80,
|
|
padding: const EdgeInsets.all(2),
|
|
child: const Center(
|
|
child: FaIcon(
|
|
FontAwesomeIcons.photoFilm,
|
|
color: Colors.white,
|
|
size: 25,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: takePicture,
|
|
// onLongPress: startVideoRecording,
|
|
key: keyTriggerButton,
|
|
child: Align(
|
|
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) const SizedBox(width: 80),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
VideoRecordingTimer(
|
|
videoRecordingStarted: _videoRecordingStarted,
|
|
maxVideoRecordingTime: maxVideoRecordingTime,
|
|
),
|
|
if (!_sharePreviewIsShown && widget.sendToGroup != 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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|