This commit is contained in:
otsmr 2025-04-13 21:44:11 +02:00
parent f0e7078ce9
commit e59e8d81a2
5 changed files with 456 additions and 237 deletions

View file

@ -1,30 +1,72 @@
import 'package:flutter/material.dart';
class MediaViewSizing extends StatelessWidget {
class MediaViewSizing extends StatefulWidget {
const MediaViewSizing(
{super.key,
this.requiredHeight,
required this.child,
this.bottomNavigation});
final double? requiredHeight;
final Widget? bottomNavigation;
final Widget child;
const MediaViewSizing(this.child, {super.key});
@override
State<MediaViewSizing> createState() => _MediaViewSizingState();
}
class _MediaViewSizingState extends State<MediaViewSizing> {
@override
Widget build(BuildContext context) {
bool needToDownSizeImage = false;
if (widget.requiredHeight != null) {
// Get the screen size and safe area padding
final screenSize = MediaQuery.of(context).size;
final safeAreaPadding = MediaQuery.of(context).padding;
// Calculate the available width and height
final availableWidth = screenSize.width;
final availableHeight =
screenSize.height - safeAreaPadding.top - safeAreaPadding.bottom;
var aspectRatioWidth = availableWidth;
var aspectRatioHeight = (aspectRatioWidth * 16) / 9;
if (aspectRatioHeight < availableHeight) {
if ((screenSize.height - widget.requiredHeight!) < aspectRatioHeight) {
needToDownSizeImage = true;
}
}
}
Widget imageChild = Align(
alignment: Alignment.topCenter,
child: SizedBox(
child: AspectRatio(
aspectRatio: 9 / 16,
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: widget.child,
),
),
),
);
Widget bottomNavigation = Container();
if (widget.bottomNavigation != null) {
if (needToDownSizeImage) {
imageChild = Expanded(child: imageChild);
bottomNavigation = widget.bottomNavigation!;
} else {
bottomNavigation = Expanded(child: widget.bottomNavigation!);
}
}
return SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Align(
alignment: Alignment.topCenter,
child: AspectRatio(
aspectRatio: 9 / 16,
// padding: EdgeInsets.symmetric(vertical: 50, horizontal: 0),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: child,
),
),
),
),
],
children: [imageChild, bottomNavigation],
),
);
}

View file

@ -200,7 +200,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
);
}
return MediaViewSizing(
Stack(
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(22),

View file

@ -294,7 +294,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() {});
},
child: MediaViewSizing(
SizedBox(
child: SizedBox(
height: currentImage.height / pixelRatio,
width: currentImage.width / pixelRatio,
child: Screenshot(

View file

@ -22,6 +22,27 @@ import 'package:twonly/src/views/chats/media_viewer_view.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_view.dart';
InputDecoration inputTextMessageDeco(BuildContext context) {
return InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide:
BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide: BorderSide(color: Colors.grey, width: 2.0),
),
);
}
class ChatListEntry extends StatelessWidget {
const ChatListEntry(
this.message, this.contact, this.lastMessageFromSameUser, this.reactions,
@ -61,6 +82,7 @@ class ChatListEntry extends StatelessWidget {
if (content is TextMessageContent) {
hasOneTextReaction = true;
if (!isEmoji(content.text)) continue;
late Widget child;
if (EmojiAnimation.animatedIcons.containsKey(content.text)) {
child = SizedBox(
@ -90,6 +112,72 @@ class ChatListEntry extends StatelessWidget {
);
}
Widget getTextResponseColumns(BuildContext context, bool right) {
List<Widget> children = [];
for (final reaction in reactions) {
MessageContent? content = MessageContent.fromJson(
reaction.kind, jsonDecode(reaction.contentJson!));
if (content is TextMessageContent) {
if (content.text.length <= 1) continue;
if (isEmoji(content.text)) continue;
var entries = [
FaIcon(
FontAwesomeIcons.reply,
size: 10,
),
SizedBox(width: 5),
Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.5,
),
child: Text(
content.text,
style: TextStyle(fontSize: 14),
textAlign: right ? TextAlign.left : TextAlign.right,
)),
];
if (!right) {
entries = entries.reversed.toList();
}
children.insert(
0,
Container(
padding: EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding: EdgeInsets.symmetric(vertical: 1, horizontal: 10),
decoration: BoxDecoration(
color: right
? const Color.fromARGB(107, 124, 77, 255)
: const Color.fromARGB(83, 68, 137, 255),
borderRadius: BorderRadius.circular(12.0),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: entries,
),
),
),
);
}
}
if (children.isEmpty) return Container();
return Column(
// mainAxisAlignment: message.messageOtherId == null
// ? MainAxisAlignment.start
// : MainAxisAlignment.end,
crossAxisAlignment:
right ? CrossAxisAlignment.start : CrossAxisAlignment.end,
children: children,
);
}
@override
Widget build(BuildContext context) {
bool right = message.messageOtherId == null;
@ -194,11 +282,21 @@ class ChatListEntry extends StatelessWidget {
padding: lastMessageFromSameUser
? EdgeInsets.only(top: 5, bottom: 0, right: 10, left: 10)
: EdgeInsets.only(top: 5, bottom: 20, right: 10, left: 10),
child: Stack(
child: Column(
mainAxisAlignment:
right ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment:
right ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Stack(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
children: [
child,
Positioned(bottom: 5, left: 5, right: 5, child: getReactionRow()),
Positioned(
bottom: 5, left: 5, right: 5, child: getReactionRow()),
],
),
getTextResponseColumns(context, !right)
],
),
),
@ -406,28 +504,7 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
onSubmitted: (_) {
_sendMessage();
},
decoration: InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding:
EdgeInsets.symmetric(horizontal: 20, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
borderSide:
BorderSide(color: Colors.grey, width: 2.0),
),
),
decoration: inputTextMessageDeco(context),
),
),
SizedBox(width: 8),

View file

@ -33,7 +33,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
Timer? progressTimer;
bool showShortReactions = false;
int selectedShortReaction = -1;
// current image related
Uint8List? imageBytes;
@ -42,12 +41,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
double progress = 0;
bool isRealTwonly = false;
bool isDownloading = false;
bool showSendTextMessageInput = false;
bool imageSaved = false;
bool imageSaving = false;
List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription;
TextEditingController textMessageController = TextEditingController();
@override
void initState() {
@ -116,6 +117,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
progress = 0;
isDownloading = false;
isRealTwonly = false;
showSendTextMessageInput = false;
});
if (content.isRealTwonly) {
@ -206,6 +208,134 @@ class _MediaViewerViewState extends State<MediaViewerView> {
_subscription.cancel();
}
Future onPressedSaveToGallery() async {
if (allMediaFiles.first.messageOtherId == null) {
return; // should not be possible
}
setState(() {
imageSaving = true;
});
encryptAndSendMessage(
null,
widget.contact.userId,
MessageJson(
kind: MessageKind.storedMediaFile,
messageId: allMediaFiles.first.messageId,
content: StoredMediaFileContent(
messageId: allMediaFiles.first.messageOtherId!,
),
timestamp: DateTime.now(),
),
pushKind: PushKind.storedMediaFile,
);
final res = await saveImageToGallery(imageBytes!);
if (res == null) {
setState(() {
imageSaving = false;
imageSaved = true;
});
}
}
Widget bottomNavigation() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (maxShowTime == 999999)
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: onPressedSaveToGallery,
child: Row(
children: [
imageSaving
? SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(strokeWidth: 1))
: imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
],
),
),
SizedBox(width: 10),
IconButton(
icon: SizedBox(
width: 30,
height: 30,
child: GridView.count(
crossAxisCount: 2,
children: List.generate(
4,
(index) {
return SizedBox(
width: 8,
height: 8,
child: Center(
child: EmojiAnimation(
emoji:
EmojiAnimation.animatedIcons.keys.toList()[index],
),
),
);
},
),
),
),
onPressed: () async {
setState(() {
showShortReactions = !showShortReactions;
});
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
SizedBox(width: 10),
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.message),
onPressed: () async {
setState(() {
showSendTextMessageInput = true;
showShortReactions = true;
});
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
SizedBox(width: 10),
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
await Navigator.push(context, MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
},
));
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -216,10 +346,19 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (imageBytes != null && (canBeSeenUntil == null || progress >= 0))
GestureDetector(
onTap: () {
if (showSendTextMessageInput) {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
return;
}
nextMediaOrExit();
},
child: MediaViewSizing(
Image.memory(
bottomNavigation: bottomNavigation(),
requiredHeight: 50,
child: Image.memory(
imageBytes!,
fit: BoxFit.contain,
frameBuilder:
@ -233,7 +372,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
height: 60,
width: 60,
child:
CircularProgressIndicator(strokeWidth: 6),
CircularProgressIndicator(strokeWidth: 2),
),
);
}),
@ -303,14 +442,114 @@ class _MediaViewerViewState extends State<MediaViewerView> {
],
),
),
AnimatedPositioned(
if (showSendTextMessageInput)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
color: context.color.surface,
padding: const EdgeInsets.only(
bottom: 10, left: 20, right: 20, top: 10),
child: Row(
children: [
IconButton(
icon: FaIcon(FontAwesomeIcons.xmark),
onPressed: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
},
),
Expanded(
child: Container(
child: TextField(
autofocus: true,
controller: textMessageController,
onEditingComplete: () {
setState(() {
showSendTextMessageInput = false;
showShortReactions = false;
});
},
decoration: inputTextMessageDeco(context),
),
),
),
IconButton(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () {
if (textMessageController.text.isNotEmpty) {
sendTextMessage(
widget.contact.userId,
TextMessageContent(
text: textMessageController.text,
responseToMessageId:
allMediaFiles.first.messageOtherId,
),
PushKind.reaction,
);
textMessageController.clear();
}
setState(() {
showSendTextMessageInput = false;
showShortReactions = false;
});
},
)
],
),
),
),
if (allMediaFiles.isNotEmpty)
ReactionButtons(
show: showShortReactions,
userId: widget.contact.userId,
responseToMessageId: allMediaFiles.first.messageOtherId!,
hide: () {
setState(() {
showShortReactions = false;
showSendTextMessageInput = false;
});
},
),
],
),
),
);
}
}
class ReactionButtons extends StatefulWidget {
const ReactionButtons(
{super.key,
required this.show,
required this.userId,
required this.responseToMessageId,
required this.hide});
final bool show;
final int userId;
final int responseToMessageId;
final Function() hide;
@override
State<ReactionButtons> createState() => _ReactionButtonsState();
}
class _ReactionButtonsState extends State<ReactionButtons> {
int selectedShortReaction = -1;
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
duration: Duration(milliseconds: 200), // Animation duration
bottom: showShortReactions ? 100 : 90,
left: showShortReactions ? 0 : 150,
right: showShortReactions ? 0 : 150,
bottom: widget.show ? 100 : 90,
left: widget.show ? 0 : 150,
right: widget.show ? 0 : 150,
curve: Curves.linearToEaseOut,
child: AnimatedOpacity(
opacity: showShortReactions ? 1.0 : 0.0, // Fade in/out
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
duration: Duration(milliseconds: 150),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -318,20 +557,17 @@ class _MediaViewerViewState extends State<MediaViewerView> {
children: List.generate(
6,
(index) {
final emoji =
EmojiAnimation.animatedIcons.keys.toList()[index];
final emoji = EmojiAnimation.animatedIcons.keys.toList()[index];
return AnimatedSize(
duration:
Duration(milliseconds: 200), // Animation duration
duration: Duration(milliseconds: 200), // Animation duration
curve: Curves.linearToEaseOut,
child: GestureDetector(
onTap: () {
sendTextMessage(
widget.contact.userId,
widget.userId,
TextMessageContent(
text: emoji,
responseToMessageId:
allMediaFiles.first.messageOtherId,
responseToMessageId: widget.responseToMessageId,
),
PushKind.reaction,
);
@ -340,7 +576,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
});
Future.delayed(Duration(milliseconds: 300), () {
setState(() {
showShortReactions = false;
widget.hide();
selectedShortReaction = -1;
});
});
},
@ -349,14 +586,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
emoji: emoji,
duration: Duration(milliseconds: 300),
startPosition: 0.0,
size: (showShortReactions) ? 40 : 10)
size: (widget.show) ? 40 : 10)
: AnimatedOpacity(
opacity: (selectedShortReaction == -1)
? 1
: 0, // Fade in/out
duration: Duration(milliseconds: 150),
child: SizedBox(
width: showShortReactions ? 40 : 10,
width: widget.show ? 40 : 10,
child: Center(
child: EmojiAnimation(
emoji: emoji,
@ -370,143 +607,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
),
),
),
if (imageBytes != null)
Positioned(
bottom: 30,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (maxShowTime == 999999)
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 {
if (allMediaFiles.first.messageOtherId == null) {
return; // should not be possible
}
setState(() {
imageSaving = true;
});
encryptAndSendMessage(
null,
widget.contact.userId,
MessageJson(
kind: MessageKind.storedMediaFile,
messageId: allMediaFiles.first.messageId,
content: StoredMediaFileContent(
messageId: allMediaFiles.first.messageOtherId!,
),
timestamp: DateTime.now(),
),
pushKind: PushKind.storedMediaFile,
);
final res = await saveImageToGallery(imageBytes!);
if (res == null) {
setState(() {
imageSaving = false;
imageSaved = true;
});
}
},
child: Row(
children: [
imageSaving
? SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(
strokeWidth: 1))
: imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
],
),
),
SizedBox(width: 10),
IconButton(
icon: SizedBox(
width: 30,
height: 30,
child: GridView.count(
crossAxisCount: 2,
children: List.generate(
4,
(index) {
return SizedBox(
width: 8,
height: 8,
child: Center(
child: EmojiAnimation(
emoji: EmojiAnimation.animatedIcons.keys
.toList()[index],
),
),
);
},
),
),
),
onPressed: () async {
setState(() {
showShortReactions = !showShortReactions;
selectedShortReaction = -1;
});
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
SizedBox(width: 10),
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.message),
onPressed: () async {
Navigator.popUntil(context, (route) => route.isFirst);
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return ChatItemDetailsView(widget.contact);
}),
);
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
SizedBox(width: 10),
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
await Navigator.push(context, MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.contact);
},
));
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
],
),
),
],
),
),
);
}
}