mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
starting with #25
This commit is contained in:
parent
f5b4e35e18
commit
5e90af79d8
6 changed files with 555 additions and 347 deletions
|
|
@ -216,6 +216,9 @@ PODS:
|
|||
- sqlite3/rtree
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
|
||||
|
|
@ -247,6 +250,7 @@ DEPENDENCIES:
|
|||
- sqlite3
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
|
|
@ -315,6 +319,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
|
||||
|
|
@ -357,6 +363,7 @@ SPEC CHECKSUMS:
|
|||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
|
||||
PODFILE CHECKSUM: 4d78ee29daee4dd5268f87f2e6b41e472cc27728
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import 'package:flutter/services.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: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';
|
||||
|
|
@ -37,6 +39,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
double baseScaleFactor = 0;
|
||||
bool cameraLoaded = false;
|
||||
bool useHighQuality = false;
|
||||
bool isVideoRecording = false;
|
||||
bool hasAudioPermission = true;
|
||||
final GlobalKey keyTriggerButton = GlobalKey();
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
late CameraController controller;
|
||||
|
|
@ -64,10 +69,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.useHighQuality != null) {
|
||||
setState(() {
|
||||
useHighQuality = user.useHighQuality!;
|
||||
});
|
||||
useHighQuality = user.useHighQuality!;
|
||||
}
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -79,6 +84,18 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
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(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void selectCamera(int sCameraId, {bool init = false}) {
|
||||
if (sCameraId >= gCameras.length) return;
|
||||
if (init) {
|
||||
|
|
@ -181,18 +198,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
final XFile picture = await controller.takePicture();
|
||||
imageBytes = loadAndDeletePictureFromFile(picture);
|
||||
} catch (e) {
|
||||
try {
|
||||
if (context.mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error taking picture: $e'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ignore: empty_catches
|
||||
} catch (e) {}
|
||||
_showCameraException(e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
|
@ -215,31 +221,22 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
imageBytes = screenshotController.capture(pixelRatio: 1);
|
||||
}
|
||||
|
||||
if (await pushImageEditor(imageBytes)) {
|
||||
if (await pushMediaEditor(imageBytes, null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// does not work??
|
||||
// if (Platform.isIOS) {
|
||||
// await controller.resumePreview();
|
||||
// } else {
|
||||
selectCamera(cameraId);
|
||||
// }
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
sharePreviewIsShown = false;
|
||||
showSelfieFlash = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> pushImageEditor(Future<Uint8List?> imageBytes) async {
|
||||
Future<bool> pushMediaEditor(
|
||||
Future<Uint8List?>? imageBytes, XFile? videFilePath) async {
|
||||
bool? shoudReturn = await Navigator.push(
|
||||
context,
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
pageBuilder: (context, a1, a2) =>
|
||||
ShareImageEditorView(imageBytes: imageBytes, sendTo: widget.sendTo),
|
||||
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
||||
videFilePath: videFilePath,
|
||||
imageBytes: imageBytes,
|
||||
sendTo: widget.sendTo,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return child;
|
||||
},
|
||||
|
|
@ -254,6 +251,13 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
Navigator.pop(context);
|
||||
return true;
|
||||
}
|
||||
selectCamera(cameraId);
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
sharePreviewIsShown = false;
|
||||
showSelfieFlash = false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -261,18 +265,93 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
controller.description.lensDirection == CameraLensDirection.front;
|
||||
|
||||
Future onPanUpdate(details) async {
|
||||
print(details);
|
||||
if (isFront) {
|
||||
return;
|
||||
}
|
||||
var diff = basePanY - details.localPosition.dy;
|
||||
if (diff > 200) diff = 200;
|
||||
if (diff < -200) diff = -200;
|
||||
var tmp = (diff / 200 * (7 * 2)).toInt() / 2;
|
||||
|
||||
var baseDiff = Platform.isAndroid ? 200.0 : 300.0;
|
||||
|
||||
if (diff > baseDiff) diff = baseDiff;
|
||||
if (diff < -baseDiff) diff = -baseDiff;
|
||||
var tmp = (diff / baseDiff * (14 * 2)).toInt() / 4;
|
||||
tmp = baseScaleFactor + tmp;
|
||||
if (tmp < 1) tmp = 1;
|
||||
updateScaleFactor(tmp);
|
||||
}
|
||||
|
||||
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.value.isRecordingVideo) return;
|
||||
|
||||
try {
|
||||
await controller.startVideoRecording();
|
||||
setState(() {
|
||||
isVideoRecording = true;
|
||||
});
|
||||
} on CameraException catch (e) {
|
||||
_showCameraException(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future stopVideoRecording() async {
|
||||
if (!controller.value.isRecordingVideo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
isVideoRecording = false;
|
||||
sharePreviewIsShown = true;
|
||||
});
|
||||
XFile? videoPath = await controller.stopVideoRecording();
|
||||
await controller.pausePreview();
|
||||
if (await pushMediaEditor(null, videoPath)) {
|
||||
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) {
|
||||
|
|
@ -281,224 +360,239 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
);
|
||||
}
|
||||
return MediaViewSizing(
|
||||
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),
|
||||
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;
|
||||
});
|
||||
print("onLongPressDown");
|
||||
// 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();
|
||||
},
|
||||
onPanUpdate: onPanUpdate,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (!galleryLoadedImageIsShown)
|
||||
CameraPreviewWidget(
|
||||
controller: controller,
|
||||
screenshotController: screenshotController,
|
||||
isFront: isFront,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onPanStart: (details) async {
|
||||
if (isFront) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
basePanY = details.localPosition.dy;
|
||||
baseScaleFactor = scaleFactor;
|
||||
});
|
||||
},
|
||||
onPanUpdate: onPanUpdate,
|
||||
onDoubleTap: () async {
|
||||
selectCamera((cameraId + 1) % 2);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!sharePreviewIsShown && widget.sendTo != null)
|
||||
SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)),
|
||||
if (!sharePreviewIsShown)
|
||||
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(
|
||||
FontAwesomeIcons.repeat,
|
||||
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
|
||||
: Color.fromARGB(158, 255, 255, 255),
|
||||
onPressed: () async {
|
||||
if (isFlashOn) {
|
||||
controller.setFlashMode(FlashMode.off);
|
||||
isFlashOn = false;
|
||||
} else {
|
||||
controller.setFlashMode(FlashMode.always);
|
||||
isFlashOn = true;
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (!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)
|
||||
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.hd_rounded,
|
||||
tooltipText: context.lang.toggleHighQuality,
|
||||
color: useHighQuality
|
||||
? Colors.white
|
||||
: const Color.fromARGB(158, 255, 255, 255),
|
||||
Icons.repeat_rounded,
|
||||
tooltipText:
|
||||
context.lang.switchFrontAndBackCamera,
|
||||
onPressed: () async {
|
||||
useHighQuality = !useHighQuality;
|
||||
setState(() {});
|
||||
var user = await getUser();
|
||||
if (user != null) {
|
||||
user.useHighQuality = useHighQuality;
|
||||
updateUser(user);
|
||||
}
|
||||
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 (!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 (!sharePreviewIsShown)
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.value.isInitialized &&
|
||||
isZoomAble &&
|
||||
!isFront)
|
||||
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: [
|
||||
GestureDetector(
|
||||
onTap: () 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 pushImageEditor(
|
||||
imageFile.readAsBytes())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
galleryLoadedImageIsShown = false;
|
||||
sharePreviewIsShown = false;
|
||||
});
|
||||
},
|
||||
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: () async {
|
||||
takePicture();
|
||||
},
|
||||
onLongPress: () async {},
|
||||
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: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 80)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
69
lib/src/views/camera/components/save_to_gallery.dart
Normal file
69
lib/src/views/camera/components/save_to_gallery.dart
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
class SaveToGalleryButton extends StatefulWidget {
|
||||
final Future<Uint8List?> Function() getMergedImage;
|
||||
final String? sendNextMediaToUserName;
|
||||
|
||||
const SaveToGalleryButton({
|
||||
super.key,
|
||||
required this.getMergedImage,
|
||||
this.sendNextMediaToUserName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SaveToGalleryButton> createState() => SaveToGalleryButtonState();
|
||||
}
|
||||
|
||||
class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
||||
bool _imageSaving = false;
|
||||
bool _imageSaved = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: _imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: _imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_imageSaving = true;
|
||||
});
|
||||
Uint8List? imageBytes = await widget.getMergedImage();
|
||||
if (imageBytes == null || !context.mounted) return;
|
||||
final res = await saveImageToGallery(imageBytes);
|
||||
if (res == null) {
|
||||
setState(() {
|
||||
_imageSaving = false;
|
||||
_imageSaved = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
_imageSaving
|
||||
? SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1))
|
||||
: _imageSaved
|
||||
? Icon(Icons.check)
|
||||
: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
if (widget.sendNextMediaToUserName == null) SizedBox(width: 10),
|
||||
if (widget.sendNextMediaToUserName == null)
|
||||
Text(_imageSaved
|
||||
? context.lang.shareImagedEditorSavedImage
|
||||
: context.lang.shareImagedEditorSaveImage)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/views/camera/components/save_to_gallery.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/notification_badge.dart';
|
||||
|
|
@ -17,6 +21,7 @@ import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
|
|||
import 'package:twonly/src/views/camera/image_editor/layers_viewer.dart';
|
||||
import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
List<Layer> layers = [];
|
||||
List<Layer> undoLayers = [];
|
||||
|
|
@ -24,8 +29,9 @@ List<Layer> removedLayers = [];
|
|||
|
||||
class ShareImageEditorView extends StatefulWidget {
|
||||
const ShareImageEditorView(
|
||||
{super.key, required this.imageBytes, this.sendTo});
|
||||
final Future<Uint8List?> imageBytes;
|
||||
{super.key, this.imageBytes, this.sendTo, this.videFilePath});
|
||||
final Future<Uint8List?>? imageBytes;
|
||||
final XFile? videFilePath;
|
||||
final Contact? sendTo;
|
||||
@override
|
||||
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
||||
|
|
@ -33,13 +39,13 @@ class ShareImageEditorView extends StatefulWidget {
|
|||
|
||||
class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||
bool imageLoadedReady = false;
|
||||
bool _imageSaved = false;
|
||||
bool _imageSaving = false;
|
||||
bool _isRealTwonly = false;
|
||||
int maxShowTime = 999999;
|
||||
String? sendNextMediaToUserName;
|
||||
double tabDownPostion = 0;
|
||||
bool sendingImage = false;
|
||||
double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
|
||||
VideoPlayerController? videoController;
|
||||
|
||||
ImageItem currentImage = ImageItem();
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
|
|
@ -48,7 +54,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
initAsync();
|
||||
loadImage(widget.imageBytes);
|
||||
if (widget.imageBytes != null) {
|
||||
loadImage(widget.imageBytes!);
|
||||
} else if (widget.videFilePath != null) {
|
||||
videoController =
|
||||
VideoPlayerController.file(File(widget.videFilePath!.path));
|
||||
videoController?.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
videoController?.setLooping(true);
|
||||
videoController?.initialize().then((_) {
|
||||
videoController!.play();
|
||||
|
||||
setState(() {});
|
||||
}).catchError((Object error) {
|
||||
print(error);
|
||||
});
|
||||
videoController?.play();
|
||||
print(widget.videFilePath!.path);
|
||||
}
|
||||
}
|
||||
|
||||
void initAsync() async {
|
||||
|
|
@ -64,6 +88,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
@override
|
||||
void dispose() {
|
||||
layers.clear();
|
||||
videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +246,23 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
];
|
||||
}
|
||||
|
||||
double widthRatio = 1, heightRatio = 1, pixelRatio = 1;
|
||||
Future pushShareImageView() async {
|
||||
Future<Uint8List?> imageBytes = getMergedImage();
|
||||
bool? wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
imageBytesFuture: imageBytes,
|
||||
isRealTwonly: _isRealTwonly,
|
||||
maxShowTime: maxShowTime,
|
||||
preselectedUser: widget.sendTo,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (wasSend != null && wasSend && context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List?> getMergedImage() async {
|
||||
Uint8List? image;
|
||||
|
|
@ -263,6 +304,26 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
}
|
||||
|
||||
Future sendImageToSinglePerson() async {
|
||||
setState(() {
|
||||
sendingImage = true;
|
||||
});
|
||||
Uint8List? imageBytes = await getMergedImage();
|
||||
if (!context.mounted) return;
|
||||
if (imageBytes == null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
Navigator.pop(context, false);
|
||||
return;
|
||||
}
|
||||
sendImage(
|
||||
[widget.sendTo!.userId],
|
||||
imageBytes,
|
||||
_isRealTwonly,
|
||||
maxShowTime,
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
|
|
@ -301,14 +362,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
child: SizedBox(
|
||||
height: currentImage.height / pixelRatio,
|
||||
width: currentImage.width / pixelRatio,
|
||||
child: Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers.where((x) => !x.isDeleted).toList(),
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (videoController != null)
|
||||
Positioned.fill(child: VideoPlayer(videoController!)),
|
||||
Screenshot(
|
||||
controller: screenshotController,
|
||||
child: LayersViewer(
|
||||
layers: layers.where((x) => !x.isDeleted).toList(),
|
||||
onUpdate: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -347,46 +414,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: _imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: _imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
_imageSaving = true;
|
||||
});
|
||||
Uint8List? imageBytes = await getMergedImage();
|
||||
if (imageBytes == null || !context.mounted) return;
|
||||
final res = await saveImageToGallery(imageBytes);
|
||||
if (res == null) {
|
||||
setState(() {
|
||||
_imageSaving = false;
|
||||
_imageSaved = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
_imageSaving
|
||||
? SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1))
|
||||
: _imageSaved
|
||||
? Icon(Icons.check)
|
||||
: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
if (sendNextMediaToUserName == null) SizedBox(width: 10),
|
||||
if (sendNextMediaToUserName == null)
|
||||
Text(_imageSaved
|
||||
? context.lang.shareImagedEditorSavedImage
|
||||
: context.lang.shareImagedEditorSaveImage)
|
||||
],
|
||||
),
|
||||
SaveToGalleryButton(
|
||||
getMergedImage: getMergedImage,
|
||||
sendNextMediaToUserName: sendNextMediaToUserName,
|
||||
),
|
||||
if (sendNextMediaToUserName != null) SizedBox(width: 10),
|
||||
if (sendNextMediaToUserName != null)
|
||||
|
|
@ -395,27 +425,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () async {
|
||||
Future<Uint8List?> imageBytes = getMergedImage();
|
||||
bool? wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
imageBytesFuture: imageBytes,
|
||||
isRealTwonly: _isRealTwonly,
|
||||
maxShowTime: maxShowTime,
|
||||
preselectedUser: widget.sendTo,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (wasSend != null && wasSend && context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
},
|
||||
onPressed: pushShareImageView,
|
||||
child: FaIcon(FontAwesomeIcons.userPlus),
|
||||
),
|
||||
if (sendNextMediaToUserName != null) SizedBox(width: 10),
|
||||
if (sendNextMediaToUserName == null) SizedBox(width: 20),
|
||||
SizedBox(width: sendNextMediaToUserName == null ? 20 : 10),
|
||||
FilledButton.icon(
|
||||
icon: sendingImage
|
||||
? SizedBox(
|
||||
|
|
@ -429,40 +442,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (sendingImage) return;
|
||||
if (widget.sendTo != null) {
|
||||
setState(() {
|
||||
sendingImage = true;
|
||||
});
|
||||
Uint8List? imageBytes = await getMergedImage();
|
||||
if (!context.mounted) return;
|
||||
if (imageBytes == null) {
|
||||
Navigator.pop(context, false);
|
||||
return;
|
||||
}
|
||||
sendImage(
|
||||
[widget.sendTo!.userId],
|
||||
imageBytes,
|
||||
_isRealTwonly,
|
||||
maxShowTime,
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
return;
|
||||
}
|
||||
Future<Uint8List?> imageBytes = getMergedImage();
|
||||
bool? wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
imageBytesFuture: imageBytes,
|
||||
isRealTwonly: _isRealTwonly,
|
||||
maxShowTime: maxShowTime,
|
||||
preselectedUser: widget.sendTo,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (wasSend != null && wasSend && context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
if (widget.sendTo == null) return pushShareImageView();
|
||||
sendImageToSinglePerson();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
|
|
|
|||
56
pubspec.lock
56
pubspec.lock
|
|
@ -313,6 +313,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -796,6 +804,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5+1"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1753,6 +1769,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.9.5"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.1"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ dependencies:
|
|||
image_picker: ^1.1.2
|
||||
http: ^1.3.0
|
||||
get: ^4.7.2
|
||||
video_player: ^2.9.5
|
||||
# avatar_maker
|
||||
# avatar_maker:
|
||||
# path: ./dependencies/avatar_maker/
|
||||
|
|
|
|||
Loading…
Reference in a new issue