mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
improved navigation page view
This commit is contained in:
parent
024def010c
commit
8e1c6a8fab
13 changed files with 982 additions and 873 deletions
|
|
@ -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<CameraPreviewWidget> createState() => _CameraPreviewWidgetState();
|
||||
}
|
||||
|
||||
class _CameraPreviewWidgetState extends State<CameraPreviewWidget> {
|
||||
@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!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/src/views/camera/camera_preview_components/send_to.dart
Normal file
57
lib/src/views/camera/camera_preview_components/send_to.dart
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
693
lib/src/views/camera/camera_preview_controller_view.dart
Normal file
693
lib/src/views/camera/camera_preview_controller_view.dart
Normal file
|
|
@ -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<CameraPreviewControllerView> createState() =>
|
||||
_CameraPreviewControllerView();
|
||||
}
|
||||
|
||||
class _CameraPreviewControllerView extends State<CameraPreviewControllerView> {
|
||||
@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<CameraPreviewView> createState() => _CameraPreviewViewState();
|
||||
}
|
||||
|
||||
class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@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<Permission, PermissionStatus> statuses = await [
|
||||
Permission.microphone,
|
||||
].request();
|
||||
if (statuses[Permission.microphone]!.isPermanentlyDenied) {
|
||||
openAppSettings();
|
||||
} else {
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<Uint8List?> 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<Uint8List?> 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<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? 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: <Widget>[
|
||||
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<Color>(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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CameraPreviewView> createState() => _CameraPreviewViewState();
|
||||
}
|
||||
|
||||
class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
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<Permission, PermissionStatus> 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<void> updateScaleFactor(double newScale) async {
|
||||
if (scaleFactor == newScale || controller == null) return;
|
||||
await controller
|
||||
?.setZoomLevel(newScale.clamp(_minAvailableZoom, _maxAvailableZoom));
|
||||
setState(() {
|
||||
scaleFactor = newScale;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Uint8List?> 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<Uint8List?> 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<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? 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: <Widget>[
|
||||
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<Color>(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<CameraPreviewViewPermission> createState() =>
|
||||
_CameraPreviewViewPermission();
|
||||
}
|
||||
|
||||
class _CameraPreviewViewPermission extends State<CameraPreviewViewPermission> {
|
||||
@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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CameraSendToView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CameraPreviewViewPermission(sendTo: widget.sendTo),
|
||||
);
|
||||
return Scaffold();
|
||||
// return Scaffold(
|
||||
// body: CameraPreviewControllerView(sendTo: widget.sendTo),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
|||
List<GalleryItem> galleryItems = [];
|
||||
Map<String, List<int>> orderedByMonth = {};
|
||||
List<String> months = [];
|
||||
bool mounted = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -135,6 +136,12 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
|||
initAsync();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
mounted = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<List<GalleryItem>> loadMemoriesDirectory() async {
|
||||
final directoryPath = await send.getMediaBaseFilePath("memories");
|
||||
final directory = Directory(directoryPath);
|
||||
|
|
@ -219,7 +226,9 @@ class GalleryMainViewState extends State<GalleryMainView> {
|
|||
}
|
||||
orderedByMonth.putIfAbsent(month, () => []).add(i);
|
||||
}
|
||||
setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -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<HomeView> 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<HomeView> {
|
||||
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<HomeView> {
|
|||
}
|
||||
}
|
||||
|
||||
@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: <Widget>[
|
||||
CameraPreviewWidget(),
|
||||
Shade(
|
||||
opacity: offsetRatio,
|
||||
),
|
||||
NotificationListener<ScrollNotification>(
|
||||
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<HomeView> {
|
|||
color: Theme.of(context).colorScheme.inverseSurface),
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments),
|
||||
label: "",
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
label: "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue