mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
start with downloading media
This commit is contained in:
parent
5cea69c224
commit
bae21c4738
15 changed files with 236 additions and 76 deletions
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path_provider/path_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/db_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -17,7 +20,7 @@ late ApiProvider apiProvider;
|
|||
void main() async {
|
||||
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.
|
||||
await settingsController.loadSettings();
|
||||
|
||||
|
|
@ -33,6 +36,11 @@ void main() async {
|
|||
}
|
||||
});
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
Hive.init(dir.path);
|
||||
|
||||
await initMediaStorage();
|
||||
|
||||
dbProvider = DbProvider();
|
||||
// Database is just a file, so this will not block the loading of the app much
|
||||
await dbProvider.ready;
|
||||
|
|
|
|||
|
|
@ -25,18 +25,31 @@ class InitialsAvatar extends StatelessWidget {
|
|||
Color avatarColor = _getColorFromUsername(
|
||||
displayName, Theme.of(context).brightness == Brightness.dark);
|
||||
|
||||
return CircleAvatar(
|
||||
backgroundColor: avatarColor,
|
||||
radius: fontSize,
|
||||
child: Text(
|
||||
Widget child = Text(
|
||||
initials,
|
||||
style: TextStyle(
|
||||
color: _getTextColor(avatarColor),
|
||||
fontWeight: FontWeight.normal,
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/json/message.dart';
|
||||
|
||||
enum MessageSendState {
|
||||
received,
|
||||
|
|
@ -11,37 +13,39 @@ enum MessageSendState {
|
|||
|
||||
class MessageSendStateIcon extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
Widget icon = Placeholder();
|
||||
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) {
|
||||
case MessageSendState.receivedOpened:
|
||||
icon = Icon(Icons.crop_square, size: 14, color: color);
|
||||
text = "Received";
|
||||
break;
|
||||
case MessageSendState.sendOpened:
|
||||
icon = Icon(
|
||||
Icons.crop_square,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
icon = FaIcon(FontAwesomeIcons.paperPlane, size: 12, color: color);
|
||||
text = "Opened";
|
||||
break;
|
||||
case MessageSendState.received:
|
||||
icon = Icon(
|
||||
Icons.square_rounded,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||
text = "Received";
|
||||
break;
|
||||
case MessageSendState.send:
|
||||
icon = Icon(
|
||||
Icons.send,
|
||||
size: 14,
|
||||
);
|
||||
icon = FaIcon(FontAwesomeIcons.solidPaperPlane, size: 12, color: color);
|
||||
text = "Send";
|
||||
break;
|
||||
case MessageSendState.sending:
|
||||
|
|
@ -51,9 +55,7 @@ class MessageSendStateIcon extends StatelessWidget {
|
|||
SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 1,
|
||||
),
|
||||
child: CircularProgressIndicator(strokeWidth: 1, color: color),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
],
|
||||
|
|
@ -62,6 +64,10 @@ class MessageSendStateIcon extends StatelessWidget {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!isDownloaded) {
|
||||
text = "Tap do load";
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
icon,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
||||
|
|
@ -22,7 +23,6 @@ class _UserContextMenuState extends State<UserContextMenu> {
|
|||
tooltip: const Text('Verify user'),
|
||||
onSelect: () {
|
||||
print('Verify user selected');
|
||||
// Add your verification logic here
|
||||
},
|
||||
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'),
|
||||
onSelect: () {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DbMessage {
|
|||
required this.messageId,
|
||||
required this.messageOtherId,
|
||||
required this.otherUserId,
|
||||
required this.messageMessageKind,
|
||||
required this.messageKind,
|
||||
required this.messageContent,
|
||||
required this.messageOpenedAt,
|
||||
required this.messageAcknowledgeByUser,
|
||||
|
|
@ -23,12 +23,17 @@ class DbMessage {
|
|||
// is this null then the message was sent from the user itself
|
||||
int? messageOtherId;
|
||||
int otherUserId;
|
||||
MessageKind messageMessageKind;
|
||||
MessageKind messageKind;
|
||||
MessageContent? messageContent;
|
||||
DateTime? messageOpenedAt;
|
||||
bool messageAcknowledgeByUser;
|
||||
bool messageAcknowledgeByServer;
|
||||
DateTime sendOrReceivedAt;
|
||||
|
||||
bool containsOtherMedia() {
|
||||
if (messageOtherId == null) return false;
|
||||
return messageKind == MessageKind.image || messageKind == MessageKind.video;
|
||||
}
|
||||
}
|
||||
|
||||
class DbMessages extends CvModelBase {
|
||||
|
|
@ -44,7 +49,7 @@ class DbMessages extends CvModelBase {
|
|||
final otherUserId = CvField<int>(columnOtherUserId);
|
||||
|
||||
static const columnMessageKind = "message_kind";
|
||||
final messageMessageKind = CvField<int>(columnMessageKind);
|
||||
final messageKind = CvField<int>(columnMessageKind);
|
||||
|
||||
static const columnMessageContentJson = "message_json";
|
||||
final messageContentJson = CvField<String?>(columnMessageContentJson);
|
||||
|
|
@ -135,7 +140,8 @@ class DbMessages extends CvModelBase {
|
|||
columnMessageAcknowledgeByServer: 1,
|
||||
columnMessageAcknowledgeByUser:
|
||||
0, // ack in case of sending corresponds to the opened flag
|
||||
columnOtherUserId: userIdFrom
|
||||
columnOtherUserId: userIdFrom,
|
||||
columnSendOrReceivedAt: DateTime.now().toIso8601String()
|
||||
});
|
||||
globalCallBackOnMessageChange(userIdFrom);
|
||||
return true;
|
||||
|
|
@ -204,7 +210,7 @@ class DbMessages extends CvModelBase {
|
|||
@override
|
||||
List<CvField> get fields => [
|
||||
messageId,
|
||||
messageMessageKind,
|
||||
messageKind,
|
||||
messageContentJson,
|
||||
messageOpenedAt,
|
||||
sendOrReceivedAt
|
||||
|
|
@ -230,7 +236,7 @@ class DbMessages extends CvModelBase {
|
|||
messageId: fromDb[i][columnMessageId],
|
||||
messageOtherId: fromDb[i][columnMessageOtherId],
|
||||
otherUserId: fromDb[i][columnOtherUserId],
|
||||
messageMessageKind:
|
||||
messageKind:
|
||||
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
|
||||
messageContent: content,
|
||||
messageOpenedAt: messageOpenedAt,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:twonly/main.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("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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,8 +152,10 @@ class ApiProvider {
|
|||
|
||||
Future<Result> _sendRequestV0(ClientToServer request) async {
|
||||
if (_channel == null) {
|
||||
if (!await connect()) {
|
||||
return Result.error(ErrorCode.InternalError);
|
||||
}
|
||||
}
|
||||
var seq = Int64(Random().nextInt(4294967296));
|
||||
while (messagesV0.containsKey(seq)) {
|
||||
seq = Int64(Random().nextInt(4294967296));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/contacts_model.dart';
|
||||
|
||||
class AlignedTextBox extends StatelessWidget {
|
||||
|
|
@ -39,8 +40,8 @@ class AlignedTextBox extends StatelessWidget {
|
|||
}
|
||||
|
||||
/// Displays detailed information about a SampleItem.
|
||||
class SampleItemDetailsView extends StatelessWidget {
|
||||
const SampleItemDetailsView({super.key, required this.user});
|
||||
class ChatItemDetailsView extends StatelessWidget {
|
||||
const ChatItemDetailsView({super.key, required this.user});
|
||||
|
||||
final Contact user;
|
||||
|
||||
|
|
@ -86,24 +87,50 @@ class SampleItemDetailsView extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
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(
|
||||
// controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type a message',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10)
|
||||
// border: OutlineInputBorder(),
|
||||
labelText: 'Enter your message',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.send),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () {
|
||||
// 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
|
||||
// },
|
||||
// ),
|
||||
// ]),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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/model/contacts_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/messages_change_provider.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/chat_item_details_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:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -47,6 +49,11 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
List<Contact> activeUsers = allUsers
|
||||
.where((x) => lastMessages.containsKey(x.userId.toInt()))
|
||||
.toList();
|
||||
activeUsers.sort((b, a) {
|
||||
return lastMessages[a.userId.toInt()]!
|
||||
.sendOrReceivedAt
|
||||
.compareTo(lastMessages[b.userId.toInt()]!.sendOrReceivedAt);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -125,24 +132,28 @@ class UserListItem extends StatefulWidget {
|
|||
class _UserListItem extends State<UserListItem> {
|
||||
int flames = 0;
|
||||
int lastMessageInSeconds = 0;
|
||||
bool isDownloaded = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//_loadAsync();
|
||||
_loadAsync();
|
||||
}
|
||||
|
||||
// Future _loadAsync() async {
|
||||
Future _loadAsync() async {
|
||||
// flames = await widget.user.getFlames();
|
||||
// lastMessageInSeconds = await widget.user.getLastMessageInSeconds();
|
||||
// setState(() {});
|
||||
// }
|
||||
|
||||
if (widget.lastMessage.containsOtherMedia()) {
|
||||
isDownloaded = await isMediaDownloaded(
|
||||
widget.lastMessage.messageContent!.downloadToken!);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
MessageSendState state;
|
||||
// int lastMessageInSeconds = widget.lastMessage.sendOrReceivedAt;
|
||||
//print(widget.lastMessage.sendOrReceivedAt);
|
||||
int lastMessageInSeconds = DateTime.now()
|
||||
.difference(widget.lastMessage.sendOrReceivedAt)
|
||||
.inSeconds;
|
||||
|
|
@ -173,7 +184,8 @@ class _UserListItem extends State<UserListItem> {
|
|||
title: Text(widget.user.displayName),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
MessageSendStateIcon(state),
|
||||
MessageSendStateIcon(
|
||||
state, isDownloaded, widget.lastMessage.messageKind),
|
||||
Text("•"),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
|
|
@ -203,11 +215,19 @@ class _UserListItem extends State<UserListItem> {
|
|||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SampleItemDetailsView(
|
||||
user: widget.user,
|
||||
),
|
||||
),
|
||||
MaterialPageRoute(builder: (context) {
|
||||
if (state == MessageSendState.received &&
|
||||
widget.lastMessage.containsOtherMedia()) {
|
||||
List<int> token =
|
||||
widget.lastMessage.messageContent!.downloadToken!;
|
||||
if (isDownloaded) {
|
||||
return MediaViewerView(widget.user);
|
||||
} else {
|
||||
tryDownloadMedia(token);
|
||||
}
|
||||
}
|
||||
return ChatItemDetailsView(user: widget.user);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:pie_menu/pie_menu.dart';
|
||||
import 'camera_preview_view.dart';
|
||||
import 'chat_list_view.dart';
|
||||
|
|
@ -79,12 +80,14 @@ class HomeViewState extends State<HomeView> {
|
|||
selectedIconTheme:
|
||||
IconThemeData(color: const Color.fromARGB(255, 255, 255, 255)),
|
||||
items: [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.chat), label: ""),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.camera_alt),
|
||||
icon: FaIcon(FontAwesomeIcons.solidComments), label: ""),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.camera),
|
||||
label: "",
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.verified_user), label: ""),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.userShield), label: ""),
|
||||
],
|
||||
onTap: (int index) {
|
||||
activePageIdx = index;
|
||||
|
|
|
|||
19
lib/src/views/media_viewer_view.dart
Normal file
19
lib/src/views/media_viewer_view.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.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/views/share_image_view.dart';
|
||||
|
||||
|
|
@ -63,10 +64,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
size: 30,
|
||||
),
|
||||
icon: Icon(Icons.close, size: 30),
|
||||
color: Colors.white,
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
|
@ -87,7 +85,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
OutlinedButton.icon(
|
||||
icon: _imageSaved
|
||||
? Icon(Icons.check)
|
||||
: Icon(Icons.save_rounded),
|
||||
: FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: _imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
|
|
@ -113,7 +111,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
const SizedBox(width: 20),
|
||||
FilledButton.icon(
|
||||
icon: Icon(Icons.send),
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
Navigator.push(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:collection';
|
|||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.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/headline.dart';
|
||||
import 'package:twonly/src/components/initialsavatar.dart';
|
||||
|
|
@ -107,7 +108,7 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
icon: Icon(Icons.send),
|
||||
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
sendImage(_selectedUserIds.toList(), widget.image);
|
||||
|
||||
|
|
|
|||
16
pubspec.lock
16
pubspec.lock
|
|
@ -490,6 +490,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -530,6 +538,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
hive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive
|
||||
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ dependencies:
|
|||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_secure_storage: ^9.2.2
|
||||
font_awesome_flutter: ^10.8.0
|
||||
gal: ^2.3.1
|
||||
google_fonts: ^6.2.1
|
||||
hive: ^2.2.3
|
||||
image: ^4.3.0
|
||||
intl: any
|
||||
introduction_screen: ^3.1.14
|
||||
|
|
|
|||
Loading…
Reference in a new issue