This commit is contained in:
otsmr 2025-11-04 21:32:43 +01:00
parent 57334d9eee
commit 94f60b5806
10 changed files with 309 additions and 208 deletions

View file

@ -5,7 +5,9 @@
- Support for groups - Support for groups
- Edit & Delete messages - Edit & Delete messages
- Switched to FFmpeg for improved video compression - Switched to FFmpeg for improved video compression
- Create images using volume buttons
- Video max. length increased to 60 seconds - Video max. length increased to 60 seconds
- New and improved emoji picker
- Removing audio after recording is possible - Removing audio after recording is possible
- Edited image is now embedded into the video - Edited image is now embedded into the video
- New context menu and other UI enhancements - New context menu and other UI enhancements

View file

@ -118,6 +118,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark, brightness: Brightness.dark,
seedColor: const Color(0xFF57CC99), seedColor: const Color(0xFF57CC99),
surface: const Color.fromARGB(255, 20, 18, 23),
surfaceContainer: const Color.fromARGB(255, 33, 30, 39),
), ),
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),

View file

@ -64,8 +64,6 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool storeMediaFilesInGallery = false; bool storeMediaFilesInGallery = false;
List<String>? lastUsedEditorEmojis;
String? lastPlanBallance; String? lastPlanBallance;
String? additionalUserInvites; String? additionalUserInvites;

View file

@ -40,9 +40,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
) )
..storeMediaFilesInGallery = ..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? false json['storeMediaFilesInGallery'] as bool? ?? false
..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
..lastPlanBallance = json['lastPlanBallance'] as String? ..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String? ..additionalUserInvites = json['additionalUserInvites'] as String?
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?) ..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
@ -93,7 +90,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions, 'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'tutorialDisplayed': instance.tutorialDisplayed, 'tutorialDisplayed': instance.tutorialDisplayed,

View file

@ -24,6 +24,7 @@ class TextLayer extends StatefulWidget {
class _TextViewState extends State<TextLayer> { class _TextViewState extends State<TextLayer> {
double initialRotation = 0; double initialRotation = 0;
bool deleteLayer = false; bool deleteLayer = false;
double localBottom = 0;
bool isDeleted = false; bool isDeleted = false;
bool elementIsScaled = false; bool elementIsScaled = false;
final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey final GlobalKey _widgetKey = GlobalKey(); // Create a GlobalKey
@ -35,9 +36,19 @@ class _TextViewState extends State<TextLayer> {
textController.text = widget.layerData.text; textController.text = widget.layerData.text;
if (widget.layerData.offset.dy == 0) {
// Set the initial offset to the center of the screen
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final mq = MediaQuery.of(context);
final globalDesiredBottom = mq.viewInsets.bottom + mq.viewPadding.bottom;
final parentBox = context.findRenderObject() as RenderBox?;
if (parentBox != null) {
final parentTopGlobal = parentBox.localToGlobal(Offset.zero).dy;
final screenHeight = mq.size.height;
localBottom = (screenHeight - globalDesiredBottom) -
parentTopGlobal -
(parentBox.size.height);
}
if (widget.layerData.offset.dy == 0) {
setState(() { setState(() {
widget.layerData.offset = Offset( widget.layerData.offset = Offset(
0, 0,
@ -47,17 +58,20 @@ class _TextViewState extends State<TextLayer> {
); );
textController.text = widget.layerData.text; textController.text = widget.layerData.text;
}); });
});
} }
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.layerData.isDeleted) return Container(); if (widget.layerData.isDeleted) return Container();
final bottom = MediaQuery.of(context).viewInsets.bottom +
MediaQuery.of(context).viewPadding.bottom;
if (widget.layerData.isEditing) { if (widget.layerData.isEditing) {
return Positioned( return Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom - 100, bottom: bottom - localBottom,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: Container(

View file

@ -1,107 +1,83 @@
import 'dart:async'; import 'dart:io';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/image_editor/data/data.dart';
import 'package:twonly/src/views/camera/image_editor/data/layer.dart'; import 'package:twonly/src/views/camera/image_editor/data/layer.dart';
class Emojis extends StatefulWidget { class EmojiPickerBottom extends StatelessWidget {
const Emojis({super.key}); const EmojiPickerBottom({super.key});
@override
State<Emojis> createState() => _EmojisState();
}
class _EmojisState extends State<Emojis> {
List<String> lastUsed = emojis;
@override
void initState() {
super.initState();
unawaited(initAsync());
}
Future<void> initAsync() async {
setState(() {
lastUsed = gUser.lastUsedEditorEmojis ?? [];
lastUsed.addAll(emojis);
});
}
Future<void> selectEmojis(String emoji) async {
await updateUserdata((user) {
if (user.lastUsedEditorEmojis == null) {
user.lastUsedEditorEmojis = [emoji];
} else {
if (user.lastUsedEditorEmojis!.contains(emoji)) {
user.lastUsedEditorEmojis!.remove(emoji);
}
user.lastUsedEditorEmojis!.insert(0, emoji);
if (user.lastUsedEditorEmojis!.length > 12) {
user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12);
}
user.lastUsedEditorEmojis!.toSet().toList();
}
return user;
});
if (!mounted) return;
Navigator.pop(
context,
EmojiLayerData(
text: emoji,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( return SingleChildScrollView(
child: Container( child: Container(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
height: 400, height: 450,
decoration: const BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32), topLeft: Radius.circular(32),
topRight: Radius.circular(32), topRight: Radius.circular(32),
), ),
color: Colors.black, color: context.color.surfaceContainer,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
blurRadius: 10.9, blurRadius: 10.9,
color: Color.fromRGBO(0, 0, 0, 0.1), color: context.color.surfaceContainer.withAlpha(25),
), ),
], ],
), ),
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
Container( Container(
height: 315, margin: const EdgeInsets.all(30),
padding: EdgeInsets.zero, decoration: BoxDecoration(
child: GridView( borderRadius: BorderRadius.circular(32),
shrinkWrap: true, color: Colors.grey,
physics: const ClampingScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 60,
),
children: lastUsed.map((String emoji) {
return GridTile(
child: GestureDetector(
onTap: () async {
await selectEmojis(emoji);
},
child: Container(
padding: EdgeInsets.zero,
alignment: Alignment.center,
child: Text(
emoji,
style: const TextStyle(fontSize: 35),
), ),
height: 3,
width: 60,
), ),
Expanded(
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
Navigator.pop(
context,
EmojiLayerData(
text: emoji.emoji,
), ),
); );
}).toList(), },
// textEditingController: _textFieldController,
config: Config(
height: 400,
locale: Localizations.localeOf(context),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.searchBar,
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
buttonIconColor: Colors.white,
),
categoryViewConfig: CategoryViewConfig(
backgroundColor: context.color.surfaceContainer,
dividerColor: Colors.white,
indicatorColor: context.color.primary,
iconColorSelected: context.color.primary,
iconColor: context.color.secondary,
),
bottomActionBarConfig: BottomActionBarConfig(
backgroundColor: context.color.surfaceContainer,
buttonColor: context.color.surfaceContainer,
buttonIconColor: context.color.secondary,
),
),
), ),
), ),
], ],

View file

@ -167,7 +167,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Emojis(); return const EmojiPickerBottom();
}, },
) as Layer?; ) as Layer?;
if (layer == null) return; if (layer == null) return;

View file

@ -11,11 +11,10 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_date_chip.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
@ -70,10 +69,8 @@ class ChatMessagesView extends StatefulWidget {
} }
class _ChatMessagesViewState extends State<ChatMessagesView> { class _ChatMessagesViewState extends State<ChatMessagesView> {
TextEditingController newMessageController = TextEditingController();
HashSet<int> alreadyReportedOpened = HashSet<int>(); HashSet<int> alreadyReportedOpened = HashSet<int>();
late Group group; late Group group;
String currentInputText = '';
late StreamSubscription<Group?> userSub; late StreamSubscription<Group?> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
StreamSubscription<List<GroupHistory>>? groupActionsSub; StreamSubscription<List<GroupHistory>>? groupActionsSub;
@ -267,21 +264,6 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
setState(() {}); setState(() {});
} }
Future<void> _sendMessage() async {
if (newMessageController.text == '') return;
await insertAndSendTextMessage(
group.groupId,
newMessageController.text,
quotesMessage?.messageId,
);
newMessageController.clear();
currentInputText = '';
quotesMessage = null;
setState(() {});
}
Future<void> scrollToMessage(String messageId) async { Future<void> scrollToMessage(String messageId) async {
final index = messages.indexWhere( final index = messages.indexWhere(
(x) => x.isMessage && x.message!.messageId == messageId, (x) => x.isMessage && x.message!.messageId == messageId,
@ -464,100 +446,15 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
), ),
), ),
if (!group.leftGroup) if (!group.leftGroup)
Padding( MessageInput(
padding: const EdgeInsets.only( group: group,
bottom: 30, quotesMessage: quotesMessage,
left: 20, textFieldFocus: textFieldFocus,
right: 20, onMessageSend: () {
top: 10, setState(() {
), quotesMessage = null;
child: Row( });
children: [
Expanded(
child: Container(
color: Colors.grey,
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),
child: Row(
children: [
const FaIcon(FontAwesomeIcons.faceSmile),
Expanded(
child: TextField(
controller: newMessageController,
focusNode: textFieldFocus,
keyboardType: TextInputType.multiline,
maxLines: 4,
minLines: 1,
onChanged: (value) {
currentInputText = value;
setState(() {});
}, },
onSubmitted: (_) {
_sendMessage();
},
decoration: InputDecoration(
hintText: context.lang.chatListDetailInput,
// contentPadding: const EdgeInsets.symmetric(
// horizontal: 20,
// vertical: 10,
// ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide(
color: Theme.of(context)
.colorScheme
.primary,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(
color: Colors.grey,
width: 2,
),
),
),
),
),
],
),
),
),
if (currentInputText != '')
IconButton(
padding: const EdgeInsets.all(15),
icon: const FaIcon(
FontAwesomeIcons.solidPaperPlane,
),
onPressed: _sendMessage,
)
else
IconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
);
},
),
],
),
), ),
], ],
), ),

View file

@ -45,7 +45,7 @@ class MessageContextMenu extends StatelessWidget {
context: context, context: context,
backgroundColor: Colors.black, backgroundColor: Colors.black,
builder: (BuildContext context) { builder: (BuildContext context) {
return const Emojis(); return const EmojiPickerBottom();
}, },
) as EmojiLayerData?; ) as EmojiLayerData?;
if (layer == null) return; if (layer == null) return;

View file

@ -0,0 +1,216 @@
import 'dart:io';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart';
class MessageInput extends StatefulWidget {
const MessageInput({
required this.group,
required this.quotesMessage,
required this.textFieldFocus,
required this.onMessageSend,
super.key,
});
final Group group;
final FocusNode textFieldFocus;
final Message? quotesMessage;
final VoidCallback onMessageSend;
@override
State<MessageInput> createState() => _MessageInputState();
}
class _MessageInputState extends State<MessageInput> {
late final TextEditingController _textFieldController;
final bool isApple = Platform.isIOS;
bool _emojiShowing = false;
Future<void> _sendMessage() async {
if (_textFieldController.text == '') return;
await insertAndSendTextMessage(
widget.group.groupId,
_textFieldController.text,
widget.quotesMessage?.messageId,
);
_textFieldController.clear();
_emojiShowing = false;
widget.onMessageSend();
setState(() {});
}
@override
void initState() {
_textFieldController = TextEditingController();
widget.textFieldFocus.addListener(_handleTextFocusChange);
super.initState();
}
@override
void dispose() {
widget.textFieldFocus.removeListener(_handleTextFocusChange);
widget.textFieldFocus.dispose();
super.dispose();
}
void _handleTextFocusChange() {
if (widget.textFieldFocus.hasFocus) {
setState(() {
_emojiShowing = false;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(
bottom: 10,
left: 10,
top: 10,
),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 3,
),
decoration: BoxDecoration(
color: context.color.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
GestureDetector(
onTap: () {
setState(() {
_emojiShowing = !_emojiShowing;
if (_emojiShowing) {
widget.textFieldFocus.unfocus();
} else {
widget.textFieldFocus.requestFocus();
}
});
},
child: Padding(
padding: const EdgeInsets.only(
top: 8,
bottom: 8,
left: 12,
right: 8,
),
child: FaIcon(
size: 20,
_emojiShowing
? FontAwesomeIcons.keyboard
: FontAwesomeIcons.faceSmile,
),
),
),
Expanded(
child: TextField(
controller: _textFieldController,
focusNode: widget.textFieldFocus,
keyboardType: TextInputType.multiline,
maxLines: 4,
minLines: 1,
onChanged: (value) {
setState(() {});
},
onSubmitted: (_) {
_sendMessage();
},
style: const TextStyle(fontSize: 17),
decoration: InputDecoration(
hintText: context.lang.chatListDetailInput,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
),
),
),
],
),
),
),
if (_textFieldController.text != '')
IconButton(
padding: const EdgeInsets.all(15),
icon: FaIcon(
color: context.color.primary,
FontAwesomeIcons.solidPaperPlane,
),
onPressed: _sendMessage,
)
else
IconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
padding: const EdgeInsets.all(15),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CameraSendToView(widget.group);
},
),
);
},
),
],
),
),
Offstage(
offstage: !_emojiShowing,
child: EmojiPicker(
textEditingController: _textFieldController,
onEmojiSelected: (category, emoji) {
setState(() {});
},
onBackspacePressed: () {
setState(() {});
},
config: Config(
height: 300,
locale: Localizations.localeOf(context),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.searchBar,
// middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.categoryBar,
),
emojiTextStyle:
TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)),
emojiViewConfig: EmojiViewConfig(
backgroundColor: context.color.surfaceContainer,
),
searchViewConfig: SearchViewConfig(
backgroundColor: context.color.surfaceContainer,
buttonIconColor: Colors.white,
),
categoryViewConfig: CategoryViewConfig(
backgroundColor: context.color.surfaceContainer,
dividerColor: Colors.white,
indicatorColor: context.color.primary,
iconColorSelected: context.color.primary,
iconColor: context.color.secondary,
),
bottomActionBarConfig: BottomActionBarConfig(
backgroundColor: context.color.surfaceContainer,
buttonColor: context.color.surfaceContainer,
buttonIconColor: context.color.secondary,
),
),
),
),
],
);
}
}