start with downloading media

This commit is contained in:
otsmr 2025-01-27 23:29:16 +01:00
parent 5cea69c224
commit bae21c4738
15 changed files with 236 additions and 76 deletions

View file

@ -1,5 +1,8 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api_provider.dart'; import 'package:twonly/src/providers/api_provider.dart';
import 'package:twonly/src/providers/db_provider.dart'; import 'package:twonly/src/providers/db_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -17,7 +20,7 @@ late ApiProvider apiProvider;
void main() async { void main() async {
final settingsController = SettingsController(SettingsService()); final settingsController = SettingsController(SettingsService());
// Load the user's peganreferred theme while the splash screen is displayed. // Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed. // This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings(); await settingsController.loadSettings();
@ -33,6 +36,11 @@ void main() async {
} }
}); });
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
await initMediaStorage();
dbProvider = DbProvider(); dbProvider = DbProvider();
// Database is just a file, so this will not block the loading of the app much // Database is just a file, so this will not block the loading of the app much
await dbProvider.ready; await dbProvider.ready;

View file

@ -25,18 +25,31 @@ class InitialsAvatar extends StatelessWidget {
Color avatarColor = _getColorFromUsername( Color avatarColor = _getColorFromUsername(
displayName, Theme.of(context).brightness == Brightness.dark); displayName, Theme.of(context).brightness == Brightness.dark);
return CircleAvatar( Widget child = Text(
backgroundColor: avatarColor,
radius: fontSize,
child: Text(
initials, initials,
style: TextStyle( style: TextStyle(
color: _getTextColor(avatarColor), color: _getTextColor(avatarColor),
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: fontSize, fontSize: fontSize,
), ),
),
); );
bool isPro = initials[0] == "T";
double proSize = (fontSize == null) ? 40 : (fontSize! * 2);
return isPro
? ClipRRect(
borderRadius: BorderRadius.circular(12.0), //or 15.0
child: Container(
height: proSize,
width: proSize,
color: avatarColor,
child: Center(child: child),
),
)
: CircleAvatar(
backgroundColor: avatarColor, radius: fontSize, child: child);
} }
Color _getTextColor(Color color) { Color _getTextColor(Color color) {

View file

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/message.dart';
enum MessageSendState { enum MessageSendState {
received, received,
@ -11,37 +13,39 @@ enum MessageSendState {
class MessageSendStateIcon extends StatelessWidget { class MessageSendStateIcon extends StatelessWidget {
final MessageSendState state; final MessageSendState state;
final MessageKind kind;
final bool isDownloaded;
const MessageSendStateIcon(this.state, {super.key}); const MessageSendStateIcon(this.state, this.isDownloaded, this.kind,
{super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget icon = Placeholder(); Widget icon = Placeholder();
String text = ""; String text = "";
Color color = Theme.of(context).colorScheme.primary;
if (kind == MessageKind.textMessage) {
color = Colors.lightBlue;
} else if (kind == MessageKind.video) {
color = Colors.deepPurple;
}
switch (state) { switch (state) {
case MessageSendState.receivedOpened: case MessageSendState.receivedOpened:
icon = Icon(Icons.crop_square, size: 14, color: color);
text = "Received";
break;
case MessageSendState.sendOpened: case MessageSendState.sendOpened:
icon = Icon( icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
Icons.crop_square,
size: 14,
color: Theme.of(context).colorScheme.primary,
);
text = "Opened"; text = "Opened";
break; break;
case MessageSendState.received: case MessageSendState.received:
icon = Icon( icon = Icon(Icons.square_rounded, size: 14, color: color);
Icons.square_rounded,
size: 14,
color: Theme.of(context).colorScheme.primary,
);
text = "Received"; text = "Received";
break; break;
case MessageSendState.send: case MessageSendState.send:
icon = Icon( icon = FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
Icons.send,
size: 14,
);
text = "Send"; text = "Send";
break; break;
case MessageSendState.sending: case MessageSendState.sending:
@ -51,9 +55,7 @@ class MessageSendStateIcon extends StatelessWidget {
SizedBox( SizedBox(
width: 10, width: 10,
height: 10, height: 10,
child: CircularProgressIndicator( child: CircularProgressIndicator(strokeWidth: 1, color: color),
strokeWidth: 1,
),
), ),
SizedBox(width: 2), SizedBox(width: 2),
], ],
@ -62,6 +64,10 @@ class MessageSendStateIcon extends StatelessWidget {
break; break;
} }
if (!isDownloaded) {
text = "Tap do load";
}
return Row( return Row(
children: [ children: [
icon, icon,

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
@ -22,7 +23,6 @@ class _UserContextMenuState extends State<UserContextMenu> {
tooltip: const Text('Verify user'), tooltip: const Text('Verify user'),
onSelect: () { onSelect: () {
print('Verify user selected'); print('Verify user selected');
// Add your verification logic here
}, },
child: const Icon(Icons.gpp_maybe_rounded), // Can be any widget child: const Icon(Icons.gpp_maybe_rounded), // Can be any widget
), ),
@ -30,9 +30,8 @@ class _UserContextMenuState extends State<UserContextMenu> {
tooltip: const Text('Send image'), tooltip: const Text('Send image'),
onSelect: () { onSelect: () {
print('Send image selected'); print('Send image selected');
// Add your image sending logic here
}, },
child: const Icon(Icons.camera_alt_rounded), // Can be any widget child: const FaIcon(FontAwesomeIcons.camera),
), ),
], ],
child: widget.child, child: widget.child,

View file

@ -11,7 +11,7 @@ class DbMessage {
required this.messageId, required this.messageId,
required this.messageOtherId, required this.messageOtherId,
required this.otherUserId, required this.otherUserId,
required this.messageMessageKind, required this.messageKind,
required this.messageContent, required this.messageContent,
required this.messageOpenedAt, required this.messageOpenedAt,
required this.messageAcknowledgeByUser, required this.messageAcknowledgeByUser,
@ -23,12 +23,17 @@ class DbMessage {
// is this null then the message was sent from the user itself // is this null then the message was sent from the user itself
int? messageOtherId; int? messageOtherId;
int otherUserId; int otherUserId;
MessageKind messageMessageKind; MessageKind messageKind;
MessageContent? messageContent; MessageContent? messageContent;
DateTime? messageOpenedAt; DateTime? messageOpenedAt;
bool messageAcknowledgeByUser; bool messageAcknowledgeByUser;
bool messageAcknowledgeByServer; bool messageAcknowledgeByServer;
DateTime sendOrReceivedAt; DateTime sendOrReceivedAt;
bool containsOtherMedia() {
if (messageOtherId == null) return false;
return messageKind == MessageKind.image || messageKind == MessageKind.video;
}
} }
class DbMessages extends CvModelBase { class DbMessages extends CvModelBase {
@ -44,7 +49,7 @@ class DbMessages extends CvModelBase {
final otherUserId = CvField<int>(columnOtherUserId); final otherUserId = CvField<int>(columnOtherUserId);
static const columnMessageKind = "message_kind"; static const columnMessageKind = "message_kind";
final messageMessageKind = CvField<int>(columnMessageKind); final messageKind = CvField<int>(columnMessageKind);
static const columnMessageContentJson = "message_json"; static const columnMessageContentJson = "message_json";
final messageContentJson = CvField<String?>(columnMessageContentJson); final messageContentJson = CvField<String?>(columnMessageContentJson);
@ -135,7 +140,8 @@ class DbMessages extends CvModelBase {
columnMessageAcknowledgeByServer: 1, columnMessageAcknowledgeByServer: 1,
columnMessageAcknowledgeByUser: columnMessageAcknowledgeByUser:
0, // ack in case of sending corresponds to the opened flag 0, // ack in case of sending corresponds to the opened flag
columnOtherUserId: userIdFrom columnOtherUserId: userIdFrom,
columnSendOrReceivedAt: DateTime.now().toIso8601String()
}); });
globalCallBackOnMessageChange(userIdFrom); globalCallBackOnMessageChange(userIdFrom);
return true; return true;
@ -204,7 +210,7 @@ class DbMessages extends CvModelBase {
@override @override
List<CvField> get fields => [ List<CvField> get fields => [
messageId, messageId,
messageMessageKind, messageKind,
messageContentJson, messageContentJson,
messageOpenedAt, messageOpenedAt,
sendOrReceivedAt sendOrReceivedAt
@ -230,7 +236,7 @@ class DbMessages extends CvModelBase {
messageId: fromDb[i][columnMessageId], messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId], messageOtherId: fromDb[i][columnMessageOtherId],
otherUserId: fromDb[i][columnOtherUserId], otherUserId: fromDb[i][columnOtherUserId],
messageMessageKind: messageKind:
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]), MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
messageContent: content, messageContent: content,
messageOpenedAt: messageOpenedAt, messageOpenedAt: messageOpenedAt,

View file

@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
@ -104,4 +106,42 @@ Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
print("check if free network connection"); print("check if free network connection");
print("Downloading: " + imageToken.toString()); print("Downloading: " + imageToken.toString());
final box = await getMediaStorage();
Uint8List imageBytes = Uint8List.fromList([0]);
box.put(imageToken.toString(), imageBytes);
}
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
final box = await getMediaStorage();
// box.put('secret', 'Hive is awesome');
return box.containsKey(mediaToken.toString());
}
Future initMediaStorage() async {
final storage = getSecureStorage();
var containsEncryptionKey =
await storage.containsKey(key: 'hive_encryption_key');
if (!containsEncryptionKey) {
var key = Hive.generateSecureKey();
await storage.write(
key: 'hive_encryption_key',
value: base64UrlEncode(key),
);
}
}
Future<Box> getMediaStorage() async {
await initMediaStorage();
final storage = getSecureStorage();
var encryptionKey =
base64Url.decode((await storage.read(key: 'hive_encryption_key'))!);
return await Hive.openBox('media_storage',
encryptionCipher: HiveAesCipher(encryptionKey));
} }

View file

@ -152,8 +152,10 @@ class ApiProvider {
Future<Result> _sendRequestV0(ClientToServer request) async { Future<Result> _sendRequestV0(ClientToServer request) async {
if (_channel == null) { if (_channel == null) {
if (!await connect()) {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
}
var seq = Int64(Random().nextInt(4294967296)); var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) { while (messagesV0.containsKey(seq)) {
seq = Int64(Random().nextInt(4294967296)); seq = Int64(Random().nextInt(4294967296));

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
class AlignedTextBox extends StatelessWidget { class AlignedTextBox extends StatelessWidget {
@ -39,8 +40,8 @@ class AlignedTextBox extends StatelessWidget {
} }
/// Displays detailed information about a SampleItem. /// Displays detailed information about a SampleItem.
class SampleItemDetailsView extends StatelessWidget { class ChatItemDetailsView extends StatelessWidget {
const SampleItemDetailsView({super.key, required this.user}); const ChatItemDetailsView({super.key, required this.user});
final Contact user; final Contact user;
@ -86,24 +87,50 @@ class SampleItemDetailsView extends StatelessWidget {
}, },
), ),
), ),
const SizedBox(
height: 10,
),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 40, left: 10, right: 10), padding:
const EdgeInsets.only(bottom: 30, left: 20, right: 20, top: 10),
child: Row(
children: [
Expanded(
child: TextField( child: TextField(
// controller: _controller,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Type a message',
contentPadding: EdgeInsets.symmetric(horizontal: 10)
// border: OutlineInputBorder(), // border: OutlineInputBorder(),
labelText: 'Enter your message', ),
suffixIcon: IconButton( ),
icon: Icon(Icons.send), ),
SizedBox(width: 8),
IconButton(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () { onPressed: () {
// Handle send action // Handle send action
}, },
), ),
],
), ),
), ),
), // Container(
// child: Row(children: [
// Padding(
// padding: const EdgeInsets.only(bottom: 40, left: 10, right: 10),
// child: TextField(
// decoration: InputDecoration(
// // border: OutlineInputBorder(),
// hintText: 'Enter your message',
// ),
// ),
// ),
// IconButton(
// icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
// onPressed: () {
// // Handle send action
// },
// ),
// ]),
// ),
], ],
), ),
); );

View file

@ -5,11 +5,13 @@ import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/components/user_context_menu.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chat_item_details_view.dart'; import 'package:twonly/src/views/chat_item_details_view.dart';
import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/home_view.dart';
import 'package:twonly/src/views/media_viewer_view.dart';
import 'package:twonly/src/views/search_username_view.dart'; import 'package:twonly/src/views/search_username_view.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -47,6 +49,11 @@ class _ChatListViewState extends State<ChatListView> {
List<Contact> activeUsers = allUsers List<Contact> activeUsers = allUsers
.where((x) => lastMessages.containsKey(x.userId.toInt())) .where((x) => lastMessages.containsKey(x.userId.toInt()))
.toList(); .toList();
activeUsers.sort((b, a) {
return lastMessages[a.userId.toInt()]!
.sendOrReceivedAt
.compareTo(lastMessages[b.userId.toInt()]!.sendOrReceivedAt);
});
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -125,24 +132,28 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> { class _UserListItem extends State<UserListItem> {
int flames = 0; int flames = 0;
int lastMessageInSeconds = 0; int lastMessageInSeconds = 0;
bool isDownloaded = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
//_loadAsync(); _loadAsync();
} }
// Future _loadAsync() async { Future _loadAsync() async {
// flames = await widget.user.getFlames(); // flames = await widget.user.getFlames();
// lastMessageInSeconds = await widget.user.getLastMessageInSeconds();
// setState(() {}); // setState(() {});
// }
if (widget.lastMessage.containsOtherMedia()) {
isDownloaded = await isMediaDownloaded(
widget.lastMessage.messageContent!.downloadToken!);
setState(() {});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
MessageSendState state; MessageSendState state;
// int lastMessageInSeconds = widget.lastMessage.sendOrReceivedAt;
//print(widget.lastMessage.sendOrReceivedAt);
int lastMessageInSeconds = DateTime.now() int lastMessageInSeconds = DateTime.now()
.difference(widget.lastMessage.sendOrReceivedAt) .difference(widget.lastMessage.sendOrReceivedAt)
.inSeconds; .inSeconds;
@ -173,7 +184,8 @@ class _UserListItem extends State<UserListItem> {
title: Text(widget.user.displayName), title: Text(widget.user.displayName),
subtitle: Row( subtitle: Row(
children: [ children: [
MessageSendStateIcon(state), MessageSendStateIcon(
state, isDownloaded, widget.lastMessage.messageKind),
Text(""), Text(""),
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
@ -203,11 +215,19 @@ class _UserListItem extends State<UserListItem> {
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (context) {
builder: (context) => SampleItemDetailsView( if (state == MessageSendState.received &&
user: widget.user, widget.lastMessage.containsOtherMedia()) {
), List<int> token =
), widget.lastMessage.messageContent!.downloadToken!;
if (isDownloaded) {
return MediaViewerView(widget.user);
} else {
tryDownloadMedia(token);
}
}
return ChatItemDetailsView(user: widget.user);
}),
); );
}, },
), ),

View file

@ -1,3 +1,4 @@
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:pie_menu/pie_menu.dart'; import 'package:pie_menu/pie_menu.dart';
import 'camera_preview_view.dart'; import 'camera_preview_view.dart';
import 'chat_list_view.dart'; import 'chat_list_view.dart';
@ -79,12 +80,14 @@ class HomeViewState extends State<HomeView> {
selectedIconTheme: selectedIconTheme:
IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)), IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)),
items: [ items: [
BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.camera_alt), icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.camera),
label: "", label: "",
), ),
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""), BottomNavigationBarItem(
icon: FaIcon(FontAwesomeIcons.userShield), label: ""),
], ],
onTap: (int index) { onTap: (int index) {
activePageIdx = index; activePageIdx = index;

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:twonly/src/model/contacts_model.dart';
class MediaViewerView extends StatefulWidget {
final Contact otherUser;
const MediaViewerView(this.otherUser, {super.key});
@override
State<MediaViewerView> createState() => _MediaViewerViewState();
}
class _MediaViewerViewState extends State<MediaViewerView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text(widget.otherUser.displayName),
);
}
}

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/share_image_view.dart'; import 'package:twonly/src/views/share_image_view.dart';
@ -63,10 +64,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
// mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
icon: Icon( icon: Icon(Icons.close, size: 30),
Icons.close,
size: 30,
),
color: Colors.white, color: Colors.white,
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
@ -87,7 +85,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
OutlinedButton.icon( OutlinedButton.icon(
icon: _imageSaved icon: _imageSaved
? Icon(Icons.check) ? Icon(Icons.check)
: Icon(Icons.save_rounded), : FaIcon(FontAwesomeIcons.floppyDisk),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: _imageSaved iconColor: _imageSaved
? Theme.of(context).colorScheme.outline ? Theme.of(context).colorScheme.outline
@ -113,7 +111,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
FilledButton.icon( FilledButton.icon(
icon: Icon(Icons.send), icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
Navigator.push( Navigator.push(
context, context,

View file

@ -2,6 +2,7 @@ import 'dart:collection';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/best_friends_selector.dart'; import 'package:twonly/src/components/best_friends_selector.dart';
import 'package:twonly/src/components/headline.dart'; import 'package:twonly/src/components/headline.dart';
import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/initialsavatar.dart';
@ -107,7 +108,7 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FilledButton.icon( FilledButton.icon(
icon: Icon(Icons.send), icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
sendImage(_selectedUserIds.toList(), widget.image); sendImage(_selectedUserIds.toList(), widget.image);

View file

@ -490,6 +490,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
font_awesome_flutter:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
url: "https://pub.dev"
source: hosted
version: "10.8.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -530,6 +538,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
http: http:
dependency: transitive dependency: transitive
description: description:

View file

@ -20,8 +20,10 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
font_awesome_flutter: ^10.8.0
gal: ^2.3.1 gal: ^2.3.1
google_fonts: ^6.2.1 google_fonts: ^6.2.1
hive: ^2.2.3
image: ^4.3.0 image: ^4.3.0
intl: any intl: any
introduction_screen: ^3.1.14 introduction_screen: ^3.1.14