mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
download and preview of images
This commit is contained in:
parent
a9572e7890
commit
f866e4315e
12 changed files with 451 additions and 162 deletions
|
|
@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
import 'package:twonly/src/app.dart';
|
import 'package:twonly/src/app.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
|
|
||||||
class DbMessage {
|
class DbMessage {
|
||||||
DbMessage({
|
DbMessage({
|
||||||
|
|
@ -15,6 +16,7 @@ class DbMessage {
|
||||||
required this.messageContent,
|
required this.messageContent,
|
||||||
required this.messageOpenedAt,
|
required this.messageOpenedAt,
|
||||||
required this.messageAcknowledgeByUser,
|
required this.messageAcknowledgeByUser,
|
||||||
|
required this.isDownloaded,
|
||||||
required this.messageAcknowledgeByServer,
|
required this.messageAcknowledgeByServer,
|
||||||
required this.sendOrReceivedAt,
|
required this.sendOrReceivedAt,
|
||||||
});
|
});
|
||||||
|
|
@ -27,6 +29,7 @@ class DbMessage {
|
||||||
MessageContent? messageContent;
|
MessageContent? messageContent;
|
||||||
DateTime? messageOpenedAt;
|
DateTime? messageOpenedAt;
|
||||||
bool messageAcknowledgeByUser;
|
bool messageAcknowledgeByUser;
|
||||||
|
bool isDownloaded;
|
||||||
bool messageAcknowledgeByServer;
|
bool messageAcknowledgeByServer;
|
||||||
DateTime sendOrReceivedAt;
|
DateTime sendOrReceivedAt;
|
||||||
|
|
||||||
|
|
@ -130,11 +133,11 @@ class DbMessages extends CvModelBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future insertOtherMessage(int userIdFrom, MessageKind kind,
|
static Future<int?> insertOtherMessage(int userIdFrom, MessageKind kind,
|
||||||
int messageId, String jsonContent) async {
|
int messageOtherId, String jsonContent) async {
|
||||||
try {
|
try {
|
||||||
await dbProvider.db!.insert(tableName, {
|
int messageId = await dbProvider.db!.insert(tableName, {
|
||||||
columnMessageOtherId: messageId,
|
columnMessageOtherId: messageOtherId,
|
||||||
columnMessageKind: kind.index,
|
columnMessageKind: kind.index,
|
||||||
columnMessageContentJson: jsonContent,
|
columnMessageContentJson: jsonContent,
|
||||||
columnMessageAcknowledgeByServer: 1,
|
columnMessageAcknowledgeByServer: 1,
|
||||||
|
|
@ -144,13 +147,26 @@ class DbMessages extends CvModelBase {
|
||||||
columnSendOrReceivedAt: DateTime.now().toIso8601String()
|
columnSendOrReceivedAt: DateTime.now().toIso8601String()
|
||||||
});
|
});
|
||||||
globalCallBackOnMessageChange(userIdFrom);
|
globalCallBackOnMessageChange(userIdFrom);
|
||||||
return true;
|
return messageId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger("contacts_model/getUsers").shout("$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(
|
static Future<DbMessage?> getLastMessagesForPreviewForUser(
|
||||||
int otherUserId) async {
|
int otherUserId) async {
|
||||||
var rows = await dbProvider.db!.query(
|
var rows = await dbProvider.db!.query(
|
||||||
|
|
@ -161,7 +177,7 @@ class DbMessages extends CvModelBase {
|
||||||
limit: 10,
|
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
|
// check if there is a message which was not ack by the server
|
||||||
List<DbMessage> notAckByServer =
|
List<DbMessage> notAckByServer =
|
||||||
|
|
@ -177,13 +193,11 @@ class DbMessages extends CvModelBase {
|
||||||
return messages[0];
|
return messages[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future acknowledgeMessageByServer(int messageId) async {
|
static Future _updateByMessageId(
|
||||||
Map<String, dynamic> valuesToUpdate = {
|
int messageId, Map<String, dynamic> data) async {
|
||||||
columnMessageAcknowledgeByServer: 1,
|
|
||||||
};
|
|
||||||
await dbProvider.db!.update(
|
await dbProvider.db!.update(
|
||||||
tableName,
|
tableName,
|
||||||
valuesToUpdate,
|
data,
|
||||||
where: "$messageId = ?",
|
where: "$messageId = ?",
|
||||||
whereArgs: [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
|
// check fromUserId to prevent spoofing
|
||||||
static Future acknowledgeMessageByUser(int fromUserId, int messageId) async {
|
static Future acknowledgeMessageByUser(int fromUserId, int messageId) async {
|
||||||
Map<String, dynamic> valuesToUpdate = {
|
Map<String, dynamic> valuesToUpdate = {
|
||||||
|
|
@ -216,7 +244,8 @@ class DbMessages extends CvModelBase {
|
||||||
sendOrReceivedAt
|
sendOrReceivedAt
|
||||||
];
|
];
|
||||||
|
|
||||||
static List<DbMessage> convertToDbMessage(List<dynamic> fromDb) {
|
static Future<List<DbMessage>> convertToDbMessage(
|
||||||
|
List<dynamic> fromDb) async {
|
||||||
try {
|
try {
|
||||||
List<DbMessage> parsedUsers = [];
|
List<DbMessage> parsedUsers = [];
|
||||||
for (int i = 0; i < fromDb.length; i++) {
|
for (int i = 0; i < fromDb.length; i++) {
|
||||||
|
|
@ -229,6 +258,16 @@ class DbMessages extends CvModelBase {
|
||||||
content = MessageContent.fromJson(
|
content = MessageContent.fromJson(
|
||||||
jsonDecode(fromDb[i][columnMessageContentJson]));
|
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(
|
parsedUsers.add(
|
||||||
DbMessage(
|
DbMessage(
|
||||||
sendOrReceivedAt:
|
sendOrReceivedAt:
|
||||||
|
|
@ -236,9 +275,9 @@ 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],
|
||||||
messageKind:
|
messageKind: messageKind,
|
||||||
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]),
|
|
||||||
messageContent: content,
|
messageContent: content,
|
||||||
|
isDownloaded: isDownloaded,
|
||||||
messageOpenedAt: messageOpenedAt,
|
messageOpenedAt: messageOpenedAt,
|
||||||
messageAcknowledgeByUser:
|
messageAcknowledgeByUser:
|
||||||
fromDb[i][columnMessageAcknowledgeByUser] == 1,
|
fromDb[i][columnMessageAcknowledgeByUser] == 1,
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ class DownloadData extends $pb.GeneratedMessage {
|
||||||
$core.List<$core.int>? uploadToken,
|
$core.List<$core.int>? uploadToken,
|
||||||
$core.int? offset,
|
$core.int? offset,
|
||||||
$core.List<$core.int>? data,
|
$core.List<$core.int>? data,
|
||||||
|
$core.bool? fin,
|
||||||
}) {
|
}) {
|
||||||
final $result = create();
|
final $result = create();
|
||||||
if (uploadToken != null) {
|
if (uploadToken != null) {
|
||||||
|
|
@ -292,6 +293,9 @@ class DownloadData extends $pb.GeneratedMessage {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
$result.data = data;
|
$result.data = data;
|
||||||
}
|
}
|
||||||
|
if (fin != null) {
|
||||||
|
$result.fin = fin;
|
||||||
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
DownloadData._() : super();
|
DownloadData._() : super();
|
||||||
|
|
@ -302,6 +306,7 @@ class DownloadData extends $pb.GeneratedMessage {
|
||||||
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY)
|
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY)
|
||||||
..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
|
..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
|
||||||
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
|
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
|
||||||
|
..aOB(4, _omitFieldNames ? '' : 'fin')
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -352,6 +357,15 @@ class DownloadData extends $pb.GeneratedMessage {
|
||||||
$core.bool hasData() => $_has(2);
|
$core.bool hasData() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearData() => clearField(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 {
|
class Response_PreKey extends $pb.GeneratedMessage {
|
||||||
|
|
|
||||||
|
|
@ -73,13 +73,14 @@ const DownloadData$json = {
|
||||||
{'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'},
|
{'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'},
|
||||||
{'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
|
{'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
|
||||||
{'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'},
|
{'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`.
|
/// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
|
||||||
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
|
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
|
||||||
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRh');
|
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu');
|
||||||
|
|
||||||
@$core.Deprecated('Use responseDescriptor instead')
|
@$core.Deprecated('Use responseDescriptor instead')
|
||||||
const Response$json = {
|
const Response$json = {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
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:hive/hive.dart';
|
||||||
|
|
@ -104,24 +105,34 @@ Future sendImage(List<Int64> userIds, String imagePath) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
|
Future tryDownloadMedia(List<int> imageToken, {bool force = false}) async {
|
||||||
print("check if free network connection");
|
if (!force) {
|
||||||
|
// TODO: create option to enable download via mobile data
|
||||||
print("Downloading: " + imageToken.toString());
|
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();
|
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]);
|
return media;
|
||||||
|
|
||||||
// box.put(imageToken.toString(), imageBytes);
|
|
||||||
box.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
|
Future<bool> isMediaDownloaded(List<int> mediaToken) async {
|
||||||
final box = await getMediaStorage();
|
final box = await getMediaStorage();
|
||||||
|
return box.containsKey("${mediaToken}_downloaded");
|
||||||
// box.put('secret', 'Hive is awesome');
|
|
||||||
|
|
||||||
return box.containsKey(mediaToken.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future initMediaStorage() async {
|
Future initMediaStorage() async {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
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:libsignal_protocol_dart/libsignal_protocol_dart.dart';
|
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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/app.dart';
|
||||||
import 'package:twonly/src/model/contacts_model.dart';
|
import 'package:twonly/src/model/contacts_model.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/model/messages_model.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.pb.dart' as client;
|
||||||
import 'package:twonly/src/proto/api/client_to_server.pbserver.dart';
|
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.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.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.dart';
|
import 'package:twonly/src/providers/api/api_utils.dart';
|
||||||
// ignore: library_prefixes
|
// ignore: library_prefixes
|
||||||
|
|
@ -18,77 +23,22 @@ import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
||||||
Future handleServerMessage(server.ServerToClient msg) async {
|
Future handleServerMessage(server.ServerToClient msg) async {
|
||||||
client.Response? response;
|
client.Response? response;
|
||||||
|
|
||||||
if (msg.v0.hasRequestNewPreKeys()) {
|
try {
|
||||||
List<PreKeyRecord> localPreKeys = await SignalHelper.getPreKeys();
|
if (msg.v0.hasRequestNewPreKeys()) {
|
||||||
|
response = await handleRequestNewPreKey();
|
||||||
List<client.Response_PreKey> prekeysList = [];
|
} else if (msg.v0.hasNewMessage()) {
|
||||||
for (int i = 0; i < localPreKeys.length; i++) {
|
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
||||||
prekeysList.add(client.Response_PreKey()
|
Int64 fromUserId = msg.v0.newMessage.fromUserId;
|
||||||
..id = Int64(localPreKeys[i].id)
|
response = await handleNewMessage(fromUserId, body);
|
||||||
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize());
|
} else if (msg.v0.hasDownloaddata()) {
|
||||||
|
response = await handleDownloadData(msg.v0.downloaddata);
|
||||||
|
} else {
|
||||||
|
Logger("handleServerMessage")
|
||||||
|
.shout("Got a new message from the server: $msg");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
var prekeys = client.Response_Prekeys(prekeys: prekeysList);
|
} catch (e) {
|
||||||
var ok = client.Response_Ok()..prekeys = prekeys;
|
response = client.Response()..error = ErrorCode.InternalError;
|
||||||
response = client.Response()..ok = ok;
|
|
||||||
} else if (msg.v0.hasNewMessage()) {
|
|
||||||
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
|
|
||||||
Int64 fromUserId = msg.v0.newMessage.fromUserId;
|
|
||||||
Message? message = await SignalHelper.getDecryptedText(fromUserId, body);
|
|
||||||
if (message != null) {
|
|
||||||
switch (message.kind) {
|
|
||||||
case MessageKind.contactRequest:
|
|
||||||
Result username = await apiProvider.getUsername(fromUserId);
|
|
||||||
if (username.isSuccess) {
|
|
||||||
Uint8List name = username.value.userdata.username;
|
|
||||||
DbContacts.insertNewContact(
|
|
||||||
utf8.decode(name), fromUserId.toInt(), true);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageKind.rejectRequest:
|
|
||||||
DbContacts.deleteUser(fromUserId.toInt());
|
|
||||||
break;
|
|
||||||
case MessageKind.acceptRequest:
|
|
||||||
DbContacts.acceptUser(fromUserId.toInt());
|
|
||||||
break;
|
|
||||||
case MessageKind.ack:
|
|
||||||
DbMessages.acknowledgeMessageByUser(
|
|
||||||
fromUserId.toInt(), message.messageId!);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (message.kind != MessageKind.textMessage &&
|
|
||||||
message.kind != MessageKind.video &&
|
|
||||||
message.kind != MessageKind.image) {
|
|
||||||
Logger("handleServerMessages")
|
|
||||||
.shout("Got unknown MessageKind $message");
|
|
||||||
} else {
|
|
||||||
String content = jsonEncode(message.content!.toJson());
|
|
||||||
await DbMessages.insertOtherMessage(
|
|
||||||
fromUserId.toInt(), message.kind, message.messageId!, content);
|
|
||||||
|
|
||||||
encryptAndSendMessage(
|
|
||||||
fromUserId,
|
|
||||||
Message(
|
|
||||||
kind: MessageKind.ack,
|
|
||||||
messageId: message.messageId!,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (message.kind == MessageKind.video ||
|
|
||||||
message.kind == MessageKind.image) {
|
|
||||||
dynamic content = message.content!;
|
|
||||||
List<int> downloadToken = content.downloadToken;
|
|
||||||
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()
|
var v0 = client.V0()
|
||||||
|
|
@ -97,3 +47,121 @@ Future handleServerMessage(server.ServerToClient msg) async {
|
||||||
|
|
||||||
apiProvider.sendResponse(ClientToServer()..v0 = v0);
|
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) {
|
||||||
|
case MessageKind.contactRequest:
|
||||||
|
Result username = await apiProvider.getUsername(fromUserId);
|
||||||
|
if (username.isSuccess) {
|
||||||
|
Uint8List name = username.value.userdata.username;
|
||||||
|
DbContacts.insertNewContact(
|
||||||
|
utf8.decode(name), fromUserId.toInt(), true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MessageKind.rejectRequest:
|
||||||
|
DbContacts.deleteUser(fromUserId.toInt());
|
||||||
|
break;
|
||||||
|
case MessageKind.acceptRequest:
|
||||||
|
DbContacts.acceptUser(fromUserId.toInt());
|
||||||
|
break;
|
||||||
|
case MessageKind.ack:
|
||||||
|
DbMessages.acknowledgeMessageByUser(
|
||||||
|
fromUserId.toInt(), message.messageId!);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (message.kind != MessageKind.textMessage &&
|
||||||
|
message.kind != MessageKind.video &&
|
||||||
|
message.kind != MessageKind.image) {
|
||||||
|
Logger("handleServerMessages")
|
||||||
|
.shout("Got unknown MessageKind $message");
|
||||||
|
} else {
|
||||||
|
String content = jsonEncode(message.content!.toJson());
|
||||||
|
int? messageId = await DbMessages.insertOtherMessage(
|
||||||
|
fromUserId.toInt(), message.kind, message.messageId!, content);
|
||||||
|
|
||||||
|
if (messageId == null) {
|
||||||
|
return client.Response()..error = ErrorCode.InternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptAndSendMessage(
|
||||||
|
fromUserId,
|
||||||
|
Message(
|
||||||
|
kind: MessageKind.ack,
|
||||||
|
messageId: message.messageId!,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (message.kind == MessageKind.video ||
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,13 @@ class ApiProvider {
|
||||||
return await _sendRequestV0(req);
|
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 {
|
Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async {
|
||||||
log.shout("fragmentate the data");
|
log.shout("fragmentate the data");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
Future<Uint8List?> encryptMessage(Message msg, Int64 target) async {
|
||||||
try {
|
try {
|
||||||
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.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';
|
||||||
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
|
import 'package:twonly/src/model/messages_model.dart';
|
||||||
|
|
||||||
class AlignedTextBox extends StatelessWidget {
|
class ChatListEntry extends StatelessWidget {
|
||||||
const AlignedTextBox({super.key, required this.text, required this.right});
|
const ChatListEntry(this.message, {super.key});
|
||||||
final String text;
|
final DbMessage message;
|
||||||
final bool right;
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Align(
|
bool right = message.messageOtherId == null;
|
||||||
alignment: right ? Alignment.centerRight : Alignment.centerLeft,
|
|
||||||
child: Padding(
|
Widget child = Container();
|
||||||
padding: EdgeInsets.all(10),
|
|
||||||
child: Container(
|
switch (message.messageKind) {
|
||||||
|
case MessageKind.textMessage:
|
||||||
|
child = Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: MediaQuery.of(context).size.width *
|
maxWidth: MediaQuery.of(context).size.width *
|
||||||
0.8, // Maximum 80% of the screen 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
|
borderRadius: BorderRadius.circular(12.0), // Set border radius
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
message.messageContent!.text!,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white, // Set text color for contrast
|
color: Colors.white, // Set text color for contrast
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.left, // Center the text
|
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.
|
/// Displays detailed information about a SampleItem.
|
||||||
class ChatItemDetailsView extends StatelessWidget {
|
class ChatItemDetailsView extends StatefulWidget {
|
||||||
const ChatItemDetailsView({super.key, required this.user});
|
const ChatItemDetailsView({super.key, required this.user});
|
||||||
|
|
||||||
final Contact 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<List<dynamic>> messages = [
|
// messages = messages.reversed.toList();
|
||||||
["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();
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Your Chat with ${user.displayName}'),
|
title: Text('Your Chat with ${widget.user.displayName}'),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: messages.length, // Number of items in the list
|
itemCount: _messages.length, // Number of items in the list
|
||||||
reverse: true,
|
reverse: true,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
return AlignedTextBox(
|
return ChatListEntry(_messages[i]);
|
||||||
text: messages[i][0], right: messages[i][1]);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -132,23 +132,10 @@ 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _loadAsync() async {
|
|
||||||
// flames = await widget.user.getFlames();
|
|
||||||
// setState(() {});
|
|
||||||
|
|
||||||
if (widget.lastMessage.containsOtherMedia()) {
|
|
||||||
isDownloaded = await isMediaDownloaded(
|
|
||||||
widget.lastMessage.messageContent!.downloadToken!);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -184,8 +171,8 @@ class _UserListItem extends State<UserListItem> {
|
||||||
title: Text(widget.user.displayName),
|
title: Text(widget.user.displayName),
|
||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
children: [
|
children: [
|
||||||
MessageSendStateIcon(
|
MessageSendStateIcon(state, widget.lastMessage.isDownloaded,
|
||||||
state, isDownloaded, widget.lastMessage.messageKind),
|
widget.lastMessage.messageKind),
|
||||||
Text("•"),
|
Text("•"),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -213,18 +200,17 @@ class _UserListItem extends State<UserListItem> {
|
||||||
),
|
),
|
||||||
leading: InitialsAvatar(displayName: widget.user.displayName),
|
leading: InitialsAvatar(displayName: widget.user.displayName),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (!widget.lastMessage.isDownloaded) {
|
||||||
|
List<int> token = widget.lastMessage.messageContent!.downloadToken!;
|
||||||
|
tryDownloadMedia(token, force: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) {
|
MaterialPageRoute(builder: (context) {
|
||||||
if (state == MessageSendState.received &&
|
if (state == MessageSendState.received &&
|
||||||
widget.lastMessage.containsOtherMedia()) {
|
widget.lastMessage.containsOtherMedia()) {
|
||||||
List<int> token =
|
return MediaViewerView(widget.user, widget.lastMessage);
|
||||||
widget.lastMessage.messageContent!.downloadToken!;
|
|
||||||
if (isDownloaded) {
|
|
||||||
return MediaViewerView(widget.user);
|
|
||||||
} else {
|
|
||||||
tryDownloadMedia(token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ChatItemDetailsView(user: widget.user);
|
return ChatItemDetailsView(user: widget.user);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,115 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
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';
|
||||||
|
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 {
|
class MediaViewerView extends StatefulWidget {
|
||||||
final Contact otherUser;
|
final Contact otherUser;
|
||||||
const MediaViewerView(this.otherUser, {super.key});
|
final DbMessage message;
|
||||||
|
const MediaViewerView(this.otherUser, this.message, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MediaViewerView> createState() => _MediaViewerViewState();
|
State<MediaViewerView> createState() => _MediaViewerViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MediaViewerViewState extends State<MediaViewerView> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
pubspec.lock
32
pubspec.lock
|
|
@ -198,6 +198,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.8"
|
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:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -238,6 +254,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
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:
|
device_info_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -730,6 +754,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
nm:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nm
|
||||||
|
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.0"
|
||||||
optional:
|
optional:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
camerawesome: ^2.1.0
|
camerawesome: ^2.1.0
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
|
connectivity_plus: ^6.1.2
|
||||||
cv: ^1.1.3
|
cv: ^1.1.3
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue