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'; import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/share_image_view.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:twonly/src/views/camera/image_editor/data/image_item.dart'; 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 layers = []; List undoLayers = []; List removedLayers = []; class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView( {super.key, this.imageBytes, this.sendTo, this.videFilePath}); final Future? imageBytes; final XFile? videFilePath; final Contact? sendTo; @override State createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { bool imageLoadedReady = 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(); @override void initState() { super.initState(); initAsync(); 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 { final user = await getUser(); if (user == null) return; if (user.defaultShowTime != null) { setState(() { maxShowTime = user.defaultShowTime!; }); } } @override void dispose() { layers.clear(); videoController?.dispose(); super.dispose(); } Future updateAsync(int userId) async { if (sendNextMediaToUserName != null) return; Contact? contact = await twonlyDatabase.contactsDao .getContactByUserId(userId) .getSingleOrNull(); if (contact != null) { sendNextMediaToUserName = getContactDisplayName(contact); } } List get actionsAtTheRight { if (layers.isNotEmpty && layers.last.isEditing && layers.last.hasCustomActionButtons) { return []; } return [ ActionButton( FontAwesomeIcons.font, tooltipText: context.lang.addTextItem, onPressed: () async { layers = layers.where((x) => !x.isDeleted).toList(); if (layers.any((x) => x.isEditing)) return; undoLayers.clear(); removedLayers.clear(); layers.add(TextLayerData( textLayersBefore: layers.whereType().length, )); setState(() {}); }, ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.pencil, tooltipText: context.lang.addDrawing, onPressed: () async { undoLayers.clear(); removedLayers.clear(); layers.add(DrawLayerData()); setState(() {}); }, ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.faceGrinWide, tooltipText: context.lang.addEmoji, onPressed: () async { EmojiLayerData? layer = await showModalBottomSheet( context: context, backgroundColor: Colors.black, builder: (BuildContext context) { return const Emojis(); }, ); if (layer == null) return; undoLayers.clear(); removedLayers.clear(); layers.add(layer); setState(() {}); }, ), const SizedBox(height: 8), NotificationBadge( count: maxShowTime == 999999 ? "∞" : maxShowTime.toString(), // count: "", child: ActionButton( FontAwesomeIcons.stopwatch, tooltipText: context.lang.protectAsARealTwonly, onPressed: () async { if (maxShowTime == 999999) { maxShowTime = 4; } else if (maxShowTime >= 22) { maxShowTime = 999999; } else { maxShowTime = maxShowTime + 8; } setState(() {}); var user = await getUser(); if (user != null) { user.defaultShowTime = maxShowTime; updateUser(user); } }, ), ), const SizedBox(height: 8), ActionButton( FontAwesomeIcons.shieldHeart, tooltipText: context.lang.protectAsARealTwonly, color: _isRealTwonly ? Theme.of(context).colorScheme.primary : Colors.white, onPressed: () async { _isRealTwonly = !_isRealTwonly; if (_isRealTwonly) { maxShowTime = 12; } setState(() {}); }, ), ]; } List get actionsAtTheTop { if (layers.isNotEmpty && layers.last.isEditing && layers.last.hasCustomActionButtons) { return []; } return [ ActionButton( FontAwesomeIcons.xmark, tooltipText: context.lang.close, onPressed: () async { Navigator.pop(context, false); }, ), Expanded(child: Container()), const SizedBox(width: 8), ActionButton( FontAwesomeIcons.rotateLeft, tooltipText: context.lang.undo, disable: layers.where((x) => x.isDeleted).length <= 2 && removedLayers.isEmpty, onPressed: () { if (removedLayers.isNotEmpty) { layers.add(removedLayers.removeLast()); setState(() {}); return; } layers = layers.where((x) => !x.isDeleted).toList(); if (layers.length <= 2) { // do not remove image layer and filter layer return; } undoLayers.add(layers.removeLast()); setState(() {}); }, ), const SizedBox(width: 8), ActionButton( FontAwesomeIcons.rotateRight, tooltipText: context.lang.redo, disable: undoLayers.isEmpty, onPressed: () { if (undoLayers.isEmpty) return; layers.add(undoLayers.removeLast()); setState(() {}); }, ), const SizedBox(width: 70) ]; } Future pushShareImageView() async { Future 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 getMergedImage() async { Uint8List? image; if (layers.length > 1) { for (var x in layers) { x.showCustomButtons = false; } setState(() {}); image = await screenshotController.capture(pixelRatio: pixelRatio); for (var x in layers) { x.showCustomButtons = true; } setState(() {}); } else if (layers.length == 1) { if (layers.first is BackgroundLayerData) { image = (layers.first as BackgroundLayerData).image.bytes; } } return image; } Future loadImage(Future imageFile) async { Uint8List? imageBytes = await imageFile; await currentImage.load(imageBytes); if (!context.mounted) return; layers.clear(); layers.add(BackgroundLayerData( image: currentImage, )); layers.add(FilterLayerData()); setState(() { imageLoadedReady = true; }); } 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; if (widget.sendTo != null) { sendNextMediaToUserName = getContactDisplayName(widget.sendTo!); } return Scaffold( backgroundColor: imageLoadedReady ? null : Colors.white.withAlpha(0), resizeToAvoidBottomInset: false, body: Stack( fit: StackFit.expand, children: [ GestureDetector( onTapDown: (details) { if (details.globalPosition.dy > 60) { tabDownPostion = details.globalPosition.dy - 60; } else { tabDownPostion = details.globalPosition.dy; } }, onTap: () { if (layers.any((x) => x.isEditing)) { return; } undoLayers.clear(); removedLayers.clear(); layers.add(TextLayerData( offset: Offset(0, tabDownPostion), textLayersBefore: layers.whereType().length, )); setState(() {}); }, child: MediaViewSizing( child: SizedBox( height: currentImage.height / pixelRatio, width: currentImage.width / pixelRatio, 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(() {}); }, ), ), ], ), ), ), ), Positioned( top: 10, left: 5, right: 0, child: SafeArea( child: Row( children: actionsAtTheTop, ), ), ), Positioned( right: 6, top: 100, child: Container( alignment: Alignment.bottomCenter, padding: const EdgeInsets.symmetric(vertical: 16), child: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: actionsAtTheRight, ), ), ), ), ], ), bottomNavigationBar: Container( color: Theme.of(context).colorScheme.surface, child: SafeArea( child: Padding( padding: const EdgeInsets.only(bottom: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SaveToGalleryButton( getMergedImage: getMergedImage, sendNextMediaToUserName: sendNextMediaToUserName, ), if (sendNextMediaToUserName != null) SizedBox(width: 10), if (sendNextMediaToUserName != null) OutlinedButton( style: OutlinedButton.styleFrom( iconColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.primary, ), onPressed: pushShareImageView, child: FaIcon(FontAwesomeIcons.userPlus), ), SizedBox(width: sendNextMediaToUserName == null ? 20 : 10), FilledButton.icon( icon: sendingImage ? SizedBox( height: 12, width: 12, child: CircularProgressIndicator( strokeWidth: 2, color: Theme.of(context).colorScheme.inversePrimary, ), ) : FaIcon(FontAwesomeIcons.solidPaperPlane), onPressed: () async { if (sendingImage) return; if (widget.sendTo == null) return pushShareImageView(); sendImageToSinglePerson(); }, style: ButtonStyle( padding: WidgetStateProperty.all( EdgeInsets.symmetric(vertical: 10, horizontal: 30), ), ), label: Text( (sendNextMediaToUserName == null) ? context.lang.shareImagedEditorShareWith : sendNextMediaToUserName!, style: TextStyle(fontSize: 17), ), ), ], ), ), ), ), ); } }