mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
467 lines
14 KiB
Dart
467 lines
14 KiB
Dart
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<Layer> layers = [];
|
|
List<Layer> undoLayers = [];
|
|
List<Layer> removedLayers = [];
|
|
|
|
class ShareImageEditorView extends StatefulWidget {
|
|
const ShareImageEditorView(
|
|
{super.key, this.imageBytes, this.sendTo, this.videFilePath});
|
|
final Future<Uint8List?>? imageBytes;
|
|
final XFile? videFilePath;
|
|
final Contact? sendTo;
|
|
@override
|
|
State<ShareImageEditorView> createState() => _ShareImageEditorView();
|
|
}
|
|
|
|
class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|
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<Widget> get actionsAtTheRight {
|
|
if (layers.isNotEmpty &&
|
|
layers.last.isEditing &&
|
|
layers.last.hasCustomActionButtons) {
|
|
return [];
|
|
}
|
|
return <Widget>[
|
|
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<TextLayerData>().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<Widget> 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<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;
|
|
|
|
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<void> loadImage(Future<Uint8List?> 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<TextLayerData>().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>(
|
|
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
|
|
),
|
|
),
|
|
label: Text(
|
|
(sendNextMediaToUserName == null)
|
|
? context.lang.shareImagedEditorShareWith
|
|
: sendNextMediaToUserName!,
|
|
style: TextStyle(fontSize: 17),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|