mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:42:54 +00:00
crop images and small improvements
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
8810ecf360
commit
b7e6cbfc2f
10 changed files with 227 additions and 124 deletions
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
## 0.1.3
|
||||
|
||||
- New: Developer settings to reduce flames
|
||||
- New: Video stabilization
|
||||
- New: Crop or rotate images before share them
|
||||
- New: Clicking on “Text Notifications” will now open the chat directly (Android only)
|
||||
- New: Developer settings to reduce flames
|
||||
- Improve: Improved troubleshooting for issues with push notifications
|
||||
- Fix: Flash not activated when starting a video recording
|
||||
- Fix: Problem sending media when a recipient has deleted their account.
|
||||
|
|
|
|||
|
|
@ -26,19 +26,19 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
Log.error('Did not update reaction as it is not an emoji!');
|
||||
return;
|
||||
}
|
||||
final msg =
|
||||
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null || msg.groupId != groupId) return;
|
||||
|
||||
try {
|
||||
if (remove) {
|
||||
await (delete(reactions)
|
||||
..where(
|
||||
(t) =>
|
||||
t.senderId.equals(contactId) &
|
||||
t.messageId.equals(messageId) &
|
||||
t.emoji.equals(emoji),
|
||||
))
|
||||
await (delete(reactions)..where(
|
||||
(t) =>
|
||||
t.senderId.equals(contactId) &
|
||||
t.messageId.equals(messageId) &
|
||||
t.emoji.equals(emoji),
|
||||
))
|
||||
.go();
|
||||
} else {
|
||||
await into(reactions).insertOnConflictUpdate(
|
||||
|
|
@ -63,18 +63,18 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
Log.error('Did not update reaction as it is not an emoji!');
|
||||
return;
|
||||
}
|
||||
final msg =
|
||||
await twonlyDB.messagesDao.getMessageById(messageId).getSingleOrNull();
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageById(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null) return;
|
||||
|
||||
try {
|
||||
await (delete(reactions)
|
||||
..where(
|
||||
(t) =>
|
||||
t.senderId.isNull() &
|
||||
t.messageId.equals(messageId) &
|
||||
t.emoji.equals(emoji),
|
||||
))
|
||||
await (delete(reactions)..where(
|
||||
(t) =>
|
||||
t.senderId.isNull() &
|
||||
t.messageId.equals(messageId) &
|
||||
t.emoji.equals(emoji),
|
||||
))
|
||||
.go();
|
||||
if (!remove) {
|
||||
await into(reactions).insert(
|
||||
|
|
@ -98,20 +98,19 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
|
|||
}
|
||||
|
||||
Stream<Reaction?> watchLastReactions(String groupId) {
|
||||
final query = (select(reactions)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
|
||||
.join(
|
||||
[
|
||||
innerJoin(
|
||||
messages,
|
||||
messages.messageId.equalsExp(reactions.messageId),
|
||||
useColumns: false,
|
||||
),
|
||||
],
|
||||
)
|
||||
..where(messages.groupId.equals(groupId))
|
||||
// ..orderBy([(t) => OrderingTerm.asc(t.createdAt)]))
|
||||
..limit(1);
|
||||
final query =
|
||||
(select(reactions)).join(
|
||||
[
|
||||
innerJoin(
|
||||
messages,
|
||||
messages.messageId.equalsExp(reactions.messageId),
|
||||
useColumns: false,
|
||||
),
|
||||
],
|
||||
)
|
||||
..where(messages.groupId.equals(groupId))
|
||||
..orderBy([OrderingTerm.desc(messages.createdAt)])
|
||||
..limit(1);
|
||||
return query.map((row) => row.readTable(reactions)).watchSingleOrNull();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@ class UserData {
|
|||
@JsonKey(defaultValue: false)
|
||||
bool requestedAudioPermission = false;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool videoStabilizationEnabled = false;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool videoStabilizationEnabled = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool showFeedbackShortcut = true;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:camera/camera.dart';
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart';
|
||||
|
|
@ -133,7 +134,7 @@ class MainCameraController {
|
|||
await cameraController?.initialize();
|
||||
await cameraController?.startImageStream(_processCameraImage);
|
||||
await cameraController?.setZoomLevel(selectedCameraDetails.scaleFactor);
|
||||
if (gUser.videoStabilizationEnabled) {
|
||||
if (gUser.videoStabilizationEnabled && !kDebugMode) {
|
||||
await cameraController?.setVideoStabilizationMode(
|
||||
VideoStabilizationMode.level1,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -109,13 +109,21 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
sendingOrLoadingImage = false;
|
||||
loadingImage = false;
|
||||
});
|
||||
videoController = VideoPlayerController.file(mediaService.originalPath);
|
||||
videoController = VideoPlayerController.file(
|
||||
mediaService.originalPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
);
|
||||
videoController?.setLooping(true);
|
||||
videoController?.initialize().then((_) async {
|
||||
await videoController!.play();
|
||||
setState(() {});
|
||||
// ignore: invalid_return_type_for_catch_error, argument_type_not_assignable_to_error_handler
|
||||
}).catchError(Log.error);
|
||||
videoController
|
||||
?.initialize()
|
||||
.then((_) async {
|
||||
await videoController!.play();
|
||||
setState(() {});
|
||||
})
|
||||
// ignore: argument_type_not_assignable_to_error_handler, invalid_return_type_for_catch_error
|
||||
.catchError(Log.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,8 +213,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
List<Widget> get actionsAtTheRight {
|
||||
if (layers.isNotEmpty &&
|
||||
layers.last.isEditing &&
|
||||
layers.last.hasCustomActionButtons) {
|
||||
(layers.first.isEditing ||
|
||||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||
return [];
|
||||
}
|
||||
return <Widget>[
|
||||
|
|
@ -246,13 +254,15 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Icons.add_reaction_outlined,
|
||||
tooltipText: context.lang.addEmoji,
|
||||
onPressed: () async {
|
||||
final layer = await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (context) {
|
||||
return const EmojiPickerBottom();
|
||||
},
|
||||
) as Layer?;
|
||||
final layer =
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black,
|
||||
builder: (context) {
|
||||
return const EmojiPickerBottom();
|
||||
},
|
||||
)
|
||||
as Layer?;
|
||||
if (layer == null) return;
|
||||
undoLayers.clear();
|
||||
removedLayers.clear();
|
||||
|
|
@ -265,19 +275,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
count: (media.type == MediaType.video)
|
||||
? '0'
|
||||
: media.displayLimitInMilliseconds == null
|
||||
? '∞'
|
||||
: (media.displayLimitInMilliseconds! ~/ 1000).toString(),
|
||||
? '∞'
|
||||
: (media.displayLimitInMilliseconds! ~/ 1000).toString(),
|
||||
child: ActionButton(
|
||||
(media.type == MediaType.video)
|
||||
? media.displayLimitInMilliseconds == null
|
||||
? Icons.repeat_rounded
|
||||
: Icons.repeat_one_rounded
|
||||
? Icons.repeat_rounded
|
||||
: Icons.repeat_one_rounded
|
||||
: Icons.timer_outlined,
|
||||
tooltipText: context.lang.protectAsARealTwonly,
|
||||
onPressed: _setImageDisplayTime,
|
||||
),
|
||||
),
|
||||
if (media.type == MediaType.video)
|
||||
if (media.type == MediaType.video) ...[
|
||||
const SizedBox(height: 8),
|
||||
ActionButton(
|
||||
(mediaService.removeAudio)
|
||||
? Icons.volume_off_rounded
|
||||
|
|
@ -296,6 +307,29 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
if (media.type == MediaType.image) ...[
|
||||
const SizedBox(height: 8),
|
||||
ActionButton(
|
||||
Icons.crop_rotate_outlined,
|
||||
tooltipText: 'Crop or rotate image',
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
final first = layers.first;
|
||||
if (first is BackgroundLayerData) {
|
||||
first.isEditing = !first.isEditing;
|
||||
}
|
||||
setState(() {});
|
||||
// await mediaService.toggleRemoveAudio();
|
||||
// if (mediaService.removeAudio) {
|
||||
// await videoController?.setVolume(0);
|
||||
// } else {
|
||||
// await videoController?.setVolume(100);
|
||||
// }
|
||||
// if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
ActionButton(
|
||||
FontAwesomeIcons.shieldHeart,
|
||||
|
|
@ -348,8 +382,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
List<Widget> get actionsAtTheTop {
|
||||
if (layers.isNotEmpty &&
|
||||
layers.last.isEditing &&
|
||||
layers.last.hasCustomActionButtons) {
|
||||
(layers.first.isEditing ||
|
||||
(layers.last.isEditing && layers.last.hasCustomActionButtons))) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
|
|
@ -411,18 +445,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
await videoController?.pause();
|
||||
if (isDisposed || !mounted) return;
|
||||
final wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
selectedGroupIds: selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
mediaStoreFuture: mediaStoreFuture,
|
||||
mediaFileService: mediaService,
|
||||
additionalData: getAdditionalData(),
|
||||
),
|
||||
),
|
||||
) as bool?;
|
||||
final wasSend =
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
selectedGroupIds: selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
mediaStoreFuture: mediaStoreFuture,
|
||||
mediaFileService: mediaService,
|
||||
additionalData: getAdditionalData(),
|
||||
),
|
||||
),
|
||||
)
|
||||
as bool?;
|
||||
if (wasSend != null && wasSend && mounted) {
|
||||
widget.mainCameraController?.onImageSend();
|
||||
Navigator.pop(context, true);
|
||||
|
|
@ -552,8 +588,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
});
|
||||
// It is important that the user can sending the image only when the image is fully loaded otherwise if the user
|
||||
// will click on send before the image is painted the screenshot will be transparent..
|
||||
_imageLoadingTimer =
|
||||
Timer.periodic(const Duration(milliseconds: 10), (timer) {
|
||||
_imageLoadingTimer = Timer.periodic(const Duration(milliseconds: 10), (
|
||||
timer,
|
||||
) {
|
||||
final imageLayer = layers.first;
|
||||
if (imageLayer is BackgroundLayerData) {
|
||||
if (imageLayer.imageLoaded) {
|
||||
|
|
@ -619,8 +656,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
await askToCloseThenClose();
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
widget.sharedFromGallery ? null : Colors.white.withAlpha(0),
|
||||
backgroundColor: widget.sharedFromGallery
|
||||
? null
|
||||
: Colors.white.withAlpha(0),
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
|
|
@ -667,8 +705,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
onPressed: pushShareImageView,
|
||||
child: const FaIcon(FontAwesomeIcons.userPlus),
|
||||
|
|
@ -681,9 +720,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
width: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.inversePrimary,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.inversePrimary,
|
||||
),
|
||||
)
|
||||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/action_button.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor/layer_data.dart';
|
||||
|
||||
class BackgroundLayer extends StatefulWidget {
|
||||
|
|
@ -29,14 +33,47 @@ class _BackgroundLayerState extends State<BackgroundLayer> {
|
|||
Widget build(BuildContext context) {
|
||||
final scImage = widget.layerData.image.image;
|
||||
if (scImage == null || scImage.image == null) return Container();
|
||||
return Container(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
padding: EdgeInsets.zero,
|
||||
color: Colors.transparent,
|
||||
child: CustomPaint(
|
||||
painter: UiImagePainter(scImage.image!),
|
||||
),
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PhotoView.customChild(
|
||||
enableRotation: true,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Container(
|
||||
width: widget.layerData.image.width.toDouble(),
|
||||
height: widget.layerData.image.height.toDouble(),
|
||||
padding: EdgeInsets.zero,
|
||||
color: Colors.transparent,
|
||||
child: CustomPaint(
|
||||
painter: UiImagePainter(scImage.image!),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.layerData.isEditing)
|
||||
Positioned(
|
||||
top: 5,
|
||||
left: 5,
|
||||
right: 50,
|
||||
child: Row(
|
||||
children: [
|
||||
ActionButton(
|
||||
FontAwesomeIcons.check,
|
||||
tooltipText: context.lang.imageEditorDrawOk,
|
||||
onPressed: () async {
|
||||
widget.layerData.isEditing = false;
|
||||
widget.onUpdate!();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ class LayersViewer extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
children: [
|
||||
...layers.whereType<BackgroundLayerData>().map((layerItem) {
|
||||
return BackgroundLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
if (!layerItem.isEditing) {
|
||||
return BackgroundLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
...layers.whereType<FilterLayerData>().map((layerItem) {
|
||||
return FilterLayer(
|
||||
|
|
@ -37,39 +41,50 @@ class LayersViewer extends StatelessWidget {
|
|||
}),
|
||||
...layers
|
||||
.where(
|
||||
(layerItem) =>
|
||||
layerItem is EmojiLayerData ||
|
||||
layerItem is DrawLayerData ||
|
||||
layerItem is LinkPreviewLayerData ||
|
||||
layerItem is TextLayerData,
|
||||
)
|
||||
(layerItem) =>
|
||||
layerItem is EmojiLayerData ||
|
||||
layerItem is DrawLayerData ||
|
||||
layerItem is LinkPreviewLayerData ||
|
||||
layerItem is TextLayerData,
|
||||
)
|
||||
.map((layerItem) {
|
||||
if (layerItem is EmojiLayerData) {
|
||||
return EmojiLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is DrawLayerData) {
|
||||
return DrawLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is TextLayerData) {
|
||||
return TextLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is LinkPreviewLayerData) {
|
||||
return LinkPreviewLayer(
|
||||
if (layerItem is EmojiLayerData) {
|
||||
return EmojiLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is DrawLayerData) {
|
||||
return DrawLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is TextLayerData) {
|
||||
return TextLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else if (layerItem is LinkPreviewLayerData) {
|
||||
return LinkPreviewLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
...layers.whereType<BackgroundLayerData>().map((layerItem) {
|
||||
if (layerItem.isEditing) {
|
||||
return BackgroundLayer(
|
||||
key: layerItem.key,
|
||||
layerData: layerItem,
|
||||
onUpdate: onUpdate,
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -75,9 +75,6 @@ class _UserListItem extends State<GroupListItem> {
|
|||
setState(() {
|
||||
_lastReaction = update;
|
||||
});
|
||||
// protectUpdateState.protect(() async {
|
||||
// await updateState(lastMessage, update, messagesNotOpened);
|
||||
// });
|
||||
});
|
||||
|
||||
_messagesNotOpenedStream = twonlyDB.messagesDao
|
||||
|
|
|
|||
|
|
@ -287,7 +287,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
var timerRequired = false;
|
||||
|
||||
if (currentMediaLocal.mediaFile.type == MediaType.video) {
|
||||
videoController = VideoPlayerController.file(currentMediaLocal.tempPath);
|
||||
videoController = VideoPlayerController.file(
|
||||
currentMediaLocal.tempPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
// only mix in case the video can be played multiple times,
|
||||
// otherwise stop the background music in case the video contains audio
|
||||
mixWithOthers:
|
||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||
),
|
||||
);
|
||||
await videoController?.setLooping(
|
||||
currentMediaLocal.mediaFile.displayLimitInMilliseconds == null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ class _VideoPlayerWrapperState extends State<VideoPlayerWrapper> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.file(widget.videoPath);
|
||||
_controller = VideoPlayerController.file(
|
||||
widget.videoPath,
|
||||
videoPlayerOptions: VideoPlayerOptions(
|
||||
mixWithOthers: true,
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
_controller.initialize().then((_) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue