download and preview of images

This commit is contained in:
otsmr 2025-01-29 00:11:00 +01:00
parent a9572e7890
commit f866e4315e
12 changed files with 451 additions and 162 deletions

View file

@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/api.dart';
class DbMessage {
DbMessage({
@ -15,6 +16,7 @@ class DbMessage {
required this.messageContent,
required this.messageOpenedAt,
required this.messageAcknowledgeByUser,
required this.isDownloaded,
required this.messageAcknowledgeByServer,
required this.sendOrReceivedAt,
});
@ -27,6 +29,7 @@ class DbMessage {
MessageContent? messageContent;
DateTime? messageOpenedAt;
bool messageAcknowledgeByUser;
bool isDownloaded;
bool messageAcknowledgeByServer;
DateTime sendOrReceivedAt;
@ -130,11 +133,11 @@ class DbMessages extends CvModelBase {
}
}
static Future insertOtherMessage(int userIdFrom, MessageKind kind,
int messageId, String jsonContent) async {
static Future<int?> insertOtherMessage(int userIdFrom, MessageKind kind,
int messageOtherId, String jsonContent) async {
try {
await dbProvider.db!.insert(tableName, {
columnMessageOtherId: messageId,
int messageId = await dbProvider.db!.insert(tableName, {
columnMessageOtherId: messageOtherId,
columnMessageKind: kind.index,
columnMessageContentJson: jsonContent,
columnMessageAcknowledgeByServer: 1,
@ -144,13 +147,26 @@ class DbMessages extends CvModelBase {
columnSendOrReceivedAt: DateTime.now().toIso8601String()
});
globalCallBackOnMessageChange(userIdFrom);
return true;
return messageId;
} catch (e) {
Logger("contacts_model/getUsers").shout("$e");
return false;
return null;
}
}
static Future<List<DbMessage>> getAllMessagesForUser(int otherUserId) async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnOtherUserId = ?",
whereArgs: [otherUserId],
orderBy: "$columnUpdatedAt DESC",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<DbMessage?> getLastMessagesForPreviewForUser(
int otherUserId) async {
var rows = await dbProvider.db!.query(
@ -161,7 +177,7 @@ class DbMessages extends CvModelBase {
limit: 10,
);
List<DbMessage> messages = convertToDbMessage(rows);
List<DbMessage> messages = await convertToDbMessage(rows);
// check if there is a message which was not ack by the server
List<DbMessage> notAckByServer =
@ -177,13 +193,11 @@ class DbMessages extends CvModelBase {
return messages[0];
}
static Future acknowledgeMessageByServer(int messageId) async {
Map<String, dynamic> valuesToUpdate = {
columnMessageAcknowledgeByServer: 1,
};
static Future _updateByMessageId(
int messageId, Map<String, dynamic> data) async {
await dbProvider.db!.update(
tableName,
valuesToUpdate,
data,
where: "$messageId = ?",
whereArgs: [messageId],
);
@ -193,6 +207,20 @@ class DbMessages extends CvModelBase {
}
}
static Future userOpenedMessage(int messageId) async {
Map<String, dynamic> data = {
columnMessageOpenedAt: DateTime.now().toIso8601String(),
};
await _updateByMessageId(messageId, data);
}
static Future acknowledgeMessageByServer(int messageId) async {
Map<String, dynamic> data = {
columnMessageAcknowledgeByServer: 1,
};
await _updateByMessageId(messageId, data);
}
// check fromUserId to prevent spoofing
static Future acknowledgeMessageByUser(int fromUserId, int messageId) async {
Map<String, dynamic> valuesToUpdate = {
@ -216,7 +244,8 @@ class DbMessages extends CvModelBase {
sendOrReceivedAt
];
static List<DbMessage> convertToDbMessage(List<dynamic> fromDb) {
static Future<List<DbMessage>> convertToDbMessage(
List<dynamic> fromDb) async {
try {
List<DbMessage> parsedUsers = [];
for (int i = 0; i < fromDb.length; i++) {
@ -229,6 +258,16 @@ class DbMessages extends CvModelBase {
content = MessageContent.fromJson(
jsonDecode(fromDb[i][columnMessageContentJson]));
}
MessageKind messageKind =
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]);
bool isDownloaded = true;
if (messageKind == MessageKind.image ||
messageKind == MessageKind.video) {
// when the media was send from the user itself the content is null
if (content != null) {
isDownloaded = await isMediaDownloaded(content.downloadToken!);
}
}
parsedUsers.add(
DbMessage(
sendOrReceivedAt:
@ -236,9 +275,9 @@ class DbMessages extends CvModelBase {
messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId],
otherUserId: fromDb[i][columnOtherUserId],
messageKind:
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
messageKind: messageKind,
messageContent: content,
isDownloaded: isDownloaded,
messageOpenedAt: messageOpenedAt,
messageAcknowledgeByUser:
fromDb[i][columnMessageAcknowledgeByUser] == 1,

View file

@ -281,6 +281,7 @@ class DownloadData extends $pb.GeneratedMessage {
$core.List<$core.int>? uploadToken,
$core.int? offset,
$core.List<$core.int>? data,
$core.bool? fin,
}) {
final $result = create();
if (uploadToken != null) {
@ -292,6 +293,9 @@ class DownloadData extends $pb.GeneratedMessage {
if (data != null) {
$result.data = data;
}
if (fin != null) {
$result.fin = fin;
}
return $result;
}
DownloadData._() : super();
@ -302,6 +306,7 @@ class DownloadData extends $pb.GeneratedMessage {
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY)
..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
..aOB(4, _omitFieldNames ? '' : 'fin')
..hasRequiredFields = false
;
@ -352,6 +357,15 @@ class DownloadData extends $pb.GeneratedMessage {
$core.bool hasData() => $_has(2);
@$pb.TagNumber(3)
void clearData() => clearField(3);
@$pb.TagNumber(4)
$core.bool get fin => $_getBF(3);
@$pb.TagNumber(4)
set fin($core.bool v) { $_setBool(3, v); }
@$pb.TagNumber(4)
$core.bool hasFin() => $_has(3);
@$pb.TagNumber(4)
void clearFin() => clearField(4);
}
class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -73,13 +73,14 @@ const DownloadData$json = {
{'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'},
{'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
{'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'},
{'1': 'fin', '3': 4, '4': 1, '5': 8, '10': 'fin'},
],
};
/// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh');
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu');
@$core.Deprecated('Use responseDescriptor instead')
const Response$json = {

View file

@ -1,5 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
@ -104,24 +105,34 @@ Future sendImage(List<Int64> userIds, String imagePath) async {
}
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
print("check if free network connection");
print("Downloading: " + imageToken.toString());
if (!force) {
// TODO: create option to enable download via mobile data
final List<ConnectivityResult> connectivityResult =
await (Connectivity().checkConnectivity());
if (connectivityResult.contains(ConnectivityResult.mobile)) {
Logger("tryDownloadMedia").info("abort download over mobile connection");
return;
}
}
Logger("tryDownloadMedia").info("Downloading: $imageToken");
apiProvider.triggerDownload(imageToken);
}
Future<Uint8List?> getDownloadedMedia(
List<int> mediaToken, int messageId) async {
final box = await getMediaStorage();
Uint8List? media = box.get("${mediaToken}_downloaded");
// box.delete(mediaToken.toString());
// box.delete("${mediaToken}_downloaded");
// box.delete("${mediaToken}_fromUserId");
// await DbMessages.userOpenedMessage(messageId);
// Uint8List imageBytes = Uint8List.fromList([0]);
// box.put(imageToken.toString(), imageBytes);
box.close();
return media;
}
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
final box = await getMediaStorage();
// box.put('secret', 'Hive is awesome');
return box.containsKey(mediaToken.toString());
return box.containsKey("${mediaToken}_downloaded");
}
Future initMediaStorage() async {

View file

@ -1,15 +1,20 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:logging/logging.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/app.dart';
import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart' as client;
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
import 'package:twonly/src/proto/api/server_to_client.pbserver.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
// ignore: library_prefixes
@ -18,21 +23,73 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
Future handleServerMessage(server.ServerToClient msg) async {
client.Response? response;
try {
if (msg.v0.hasRequestNewPreKeys()) {
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
List<client.Response_PreKey> prekeysList = [];
for (int i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
}
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
response = client.Response()..ok = ok;
response = await handleRequestNewPreKey();
} else if (msg.v0.hasNewMessage()) {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
Int64 fromUserId = msg.v0.newMessage.fromUserId;
response = await handleNewMessage(fromUserId, body);
} else if (msg.v0.hasDownloaddata()) {
response = await handleDownloadData(msg.v0.downloaddata);
} else {
Logger("handleServerMessage")
.shout("Got a new message from the server: $msg");
return;
}
} catch (e) {
response = client.Response()..error = ErrorCode.InternalError;
}
var v0 = client.V0()
..seq = msg.v0.seq
..response = response;
apiProvider.sendResponse(ClientToServer()..v0 = v0);
}
Future<client.Response> handleDownloadData(DownloadData data) async {
debugPrint("Downloading: ${data.uploadToken} ${data.fin}");
final box = await getMediaStorage();
String boxId = data.uploadToken.toString();
Uint8List? buffered = box.get(boxId);
Uint8List downloadedBytes;
if (buffered != null) {
if (data.offset != buffered.length) {
return client.Response()..error = ErrorCode.BadRequest;
}
var b = BytesBuilder();
b.add(buffered);
b.add(data.data);
downloadedBytes = b.takeBytes();
} else {
downloadedBytes = Uint8List.fromList(data.data);
}
if (data.fin) {
SignalHelper.getSignalStore();
int fromUserId = box.get("${data.uploadToken}_fromUserId")!;
Uint8List? rawBytes =
await SignalHelper.decryptBytes(downloadedBytes, Int64(fromUserId));
if (rawBytes != null) {
box.put("${data.uploadToken}_downloaded", rawBytes);
}
box.delete(boxId);
globalCallBackOnMessageChange(fromUserId);
} else {
box.put(boxId, downloadedBytes);
}
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Future<client.Response> handleNewMessage(
Int64 fromUserId, Uint8List body) async {
Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
if (message != null) {
switch (message.kind) {
@ -62,9 +119,13 @@ Future handleServerMessage(server.ServerToClient msg) async {
.shout("Got unknown MessageKind $message");
} else {
String content = jsonEncode(message.content!.toJson());
await DbMessages.insertOtherMessage(
int? messageId = await DbMessages.insertOtherMessage(
fromUserId.toInt(), message.kind, message.messageId!, content);
if (messageId == null) {
return client.Response()..error = ErrorCode.InternalError;
}
encryptAndSendMessage(
fromUserId,
Message(
@ -78,22 +139,29 @@ Future handleServerMessage(server.ServerToClient msg) async {
message.kind == MessageKind.image) {
dynamic content = message.content!;
List<int> downloadToken = content.downloadToken;
Box box = await getMediaStorage();
box.put("${downloadToken}_fromUserId", fromUserId.toInt());
tryDownloadMedia(downloadToken);
}
}
}
}
var ok = client.Response_Ok()..none = true;
response = client.Response()..ok = ok;
} else {
Logger("handleServerMessage")
.shout("Got a new message from the server: $msg");
return;
}
var v0 = client.V0()
..seq = msg.v0.seq
..response = response;
apiProvider.sendResponse(ClientToServer()..v0 = v0);
return client.Response()..ok = ok;
}
Future<client.Response> handleRequestNewPreKey() async {
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
List<client.Response_PreKey> prekeysList = [];
for (int i = 0; i < localPreKeys.length; i++) {
prekeysList.add(client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
}
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
var ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
}

View file

@ -255,6 +255,13 @@ class ApiProvider {
return await _sendRequestV0(req);
}
Future<Result> triggerDownload(List<int> token) async {
var get = ApplicationData_DownloadData()..uploadToken = token;
var appData = ApplicationData()..downloaddata = get;
var req = createClientToServerFromApplicationData(appData);
return await _sendRequestV0(req);
}
Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async {
log.shout("fragmentate the data");

View file

@ -211,6 +211,36 @@ Future<Uint8List?> encryptBytes(Uint8List bytes, Int64 target) async {
}
}
Future<Uint8List?> decryptBytes(Uint8List bytes, Int64 target) async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
SessionCipher session = SessionCipher.fromStore(
signalStore, SignalProtocolAddress(target.toString(), defaultDeviceId));
List<Uint8List>? msgs = removeLastFourBytes(bytes);
if (msgs == null) return null;
Uint8List body = msgs[0];
int type = bytesToInt(msgs[1]);
Uint8List plaintext;
if (type == CiphertextMessage.prekeyType) {
PreKeySignalMessage pre = PreKeySignalMessage(body);
plaintext = await session.decrypt(pre);
} else if (type == CiphertextMessage.whisperType) {
SignalMessage signalMsg = SignalMessage.fromSerialized(body);
plaintext = await session.decryptFromSignal(signalMsg);
} else {
return null;
}
List<int>? plainBytes = gzip.decode(Uint8List.fromList(plaintext));
return Uint8List.fromList(plainBytes);
} catch (e) {
Logger("utils/signal").shout(e.toString());
return null;
}
}
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;

View file

@ -1,18 +1,21 @@
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/json/message.dart';
import 'package:twonly/src/model/messages_model.dart';
class AlignedTextBox extends StatelessWidget {
const AlignedTextBox({super.key, required this.text, required this.right});
final String text;
final bool right;
class ChatListEntry extends StatelessWidget {
const ChatListEntry(this.message, {super.key});
final DbMessage message;
@override
Widget build(BuildContext context) {
return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(10),
child: Container(
bool right = message.messageOtherId == null;
Widget child = Container();
switch (message.messageKind) {
case MessageKind.textMessage:
child = Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width *
0.8, // Maximum 80% of the screen width
@ -26,64 +29,65 @@ class AlignedTextBox extends StatelessWidget {
borderRadius: BorderRadius.circular(12.0), // Set border radius
),
child: Text(
text,
message.messageContent!.text!,
style: TextStyle(
color: Colors.white, // Set text color for contrast
fontSize: 17,
),
textAlign: TextAlign.left, // Center the text
),
),
),
);
break;
default:
return Container();
}
return Align(
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
child: Padding(padding: EdgeInsets.all(10), child: child),
);
}
}
/// Displays detailed information about a SampleItem.
class ChatItemDetailsView extends StatelessWidget {
class ChatItemDetailsView extends StatefulWidget {
const ChatItemDetailsView({super.key, required this.user});
final Contact user;
@override
State<ChatItemDetailsView> createState() => _ChatItemDetailsViewState();
}
class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
List<DbMessage> _messages = [];
@override
void initState() {
super.initState();
_loadAsync();
}
Future _loadAsync() async {
_messages =
await DbMessages.getAllMessagesForUser(widget.user.userId.toInt());
}
@override
Widget build(BuildContext context) {
List<List<dynamic>> messages = [
["Hallo", true],
["Wie geht's?", false],
["Das ist ein Test.", true],
["Flutter ist großartig!", false],
["Ich liebe Programmieren.", true],
["Das Wetter ist schön.", false],
["Hast du Pläne für heute?", true],
["Ich mag Pizza.", false],
["Lass uns einen Film schauen.", false],
["Das ist interessant.", false],
["Ich bin müde.", true],
["Was machst du gerade?", true],
["Ich habe ein neues Hobby.", true],
["Das ist eine lange Nachricht.", false],
["Ich freue mich auf das Wochenende.", true],
["Das ist eine zufällige Nachricht.", false],
["Ich lerne Dart.", true],
["Wie war dein Tag?", true],
["Ich genieße die Natur.", true],
["Das ist ein schöner Ort.", false],
["Meine letzte Nachricht.", false],
];
messages = messages.reversed.toList();
// messages = messages.reversed.toList();
return Scaffold(
appBar: AppBar(
title: Text('Your Chat with ${user.displayName}'),
title: Text('Your Chat with ${widget.user.displayName}'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: messages.length, // Number of items in the list
itemCount: _messages.length, // Number of items in the list
reverse: true,
itemBuilder: (context, i) {
return AlignedTextBox(
text: messages[i][0], right: messages[i][1]);
return ChatListEntry(_messages[i]);
},
),
),

View file

@ -132,23 +132,10 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> {
int flames = 0;
int lastMessageInSeconds = 0;
bool isDownloaded = true;
@override
void initState() {
super.initState();
_loadAsync();
}
Future _loadAsync() async {
// flames = await widget.user.getFlames();
// setState(() {});
if (widget.lastMessage.containsOtherMedia()) {
isDownloaded = await isMediaDownloaded(
widget.lastMessage.messageContent!.downloadToken!);
setState(() {});
}
}
@override
@ -184,8 +171,8 @@ class _UserListItem extends State<UserListItem> {
title: Text(widget.user.displayName),
subtitle: Row(
children: [
MessageSendStateIcon(
state, isDownloaded, widget.lastMessage.messageKind),
MessageSendStateIcon(state, widget.lastMessage.isDownloaded,
widget.lastMessage.messageKind),
Text(""),
const SizedBox(width: 5),
Text(
@ -213,18 +200,17 @@ class _UserListItem extends State<UserListItem> {
),
leading: InitialsAvatar(displayName: widget.user.displayName),
onTap: () {
if (!widget.lastMessage.isDownloaded) {
List<int> token = widget.lastMessage.messageContent!.downloadToken!;
tryDownloadMedia(token, force: true);
return;
}
Navigator.push(
context,
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 MediaViewerView(widget.user, widget.lastMessage);
}
return ChatItemDetailsView(user: widget.user);
}),

View file

@ -1,19 +1,115 @@
import 'dart:typed_data';
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/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class MediaViewerView extends StatefulWidget {
final Contact otherUser;
const MediaViewerView(this.otherUser, {super.key});
final DbMessage message;
const MediaViewerView(this.otherUser, this.message, {super.key});
@override
State<MediaViewerView> createState() => _MediaViewerViewState();
}
class _MediaViewerViewState extends State<MediaViewerView> {
Uint8List? _imageByte;
@override
void initState() {
super.initState();
_initAsync();
}
Future _initAsync() async {
List<int> token = widget.message.messageContent!.downloadToken!;
_imageByte = await getDownloadedMedia(token, widget.message.messageId);
print(_imageByte);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text(widget.otherUser.displayName),
body: Stack(
fit: StackFit.expand,
children: [
Positioned(
top: 0,
// bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 50),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: (_imageByte != null)
? Image.memory(
_imageByte!,
fit: BoxFit.contain,
)
: CircularProgressIndicator()),
),
),
_imageByte != null
? Positioned(
left: 10,
top: 60,
child: Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.close, size: 30),
color: Colors.white,
onPressed: () async {
Navigator.pop(context);
},
),
],
),
)
: Container(),
_imageByte != null
? Positioned(
bottom: 70,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 20),
FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) =>
// ShareImageView(image: widget.image)),
// );
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
),
label: Text(
AppLocalizations.of(context)!
.shareImagedEditorShareWith,
style: TextStyle(fontSize: 17),
),
),
],
),
)
: Container(),
],
),
);
}
}

View file

@ -198,6 +198,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.8"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
url: "https://pub.dev"
source: hosted
version: "6.1.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
convert:
dependency: transitive
description:
@ -238,6 +254,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus:
dependency: transitive
description:
@ -730,6 +754,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
optional:
dependency: transitive
description:

View file

@ -12,6 +12,7 @@ environment:
dependencies:
camerawesome: ^2.1.0
collection: ^1.18.0
connectivity_plus: ^6.1.2
cv: ^1.1.3
fixnum: ^1.1.1
flutter: