mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:28:40 +00:00
backup creation and upload does work #121
This commit is contained in:
parent
fcc2c9fb9e
commit
9295de7b76
25 changed files with 705 additions and 153 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
# Set the source directory
|
||||
|
||||
protoc --proto_path="./lib/src/model/protobuf/backup/" --dart_out="./lib/src/model/protobuf/backup/" "backup.proto"
|
||||
|
||||
|
||||
|
||||
SRC_DIR="../twonly-server/twonly/src/"
|
||||
|
||||
DST_DIR="$(pwd)/lib/src/model/protobuf/"
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ class SecureStorageKeys {
|
|||
static const String apiAuthToken = "api_auth_token";
|
||||
static const String googleFcm = "google_fcm";
|
||||
static const String userData = "userData";
|
||||
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,25 +23,39 @@ class UserData {
|
|||
String displayName;
|
||||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
int? avatarCounter;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int avatarCounter = 0;
|
||||
|
||||
// --- SUBSCRIPTION DTA ---
|
||||
|
||||
@JsonKey(defaultValue: "Preview")
|
||||
String subscriptionPlan;
|
||||
DateTime? lastImageSend;
|
||||
int? todaysImageCounter;
|
||||
|
||||
// --- SETTINGS ---
|
||||
|
||||
@JsonKey(defaultValue: ThemeMode.system)
|
||||
ThemeMode themeMode = ThemeMode.system;
|
||||
|
||||
int? defaultShowTime;
|
||||
@JsonKey(defaultValue: "Preview")
|
||||
String subscriptionPlan;
|
||||
bool? useHighQuality;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool useHighQuality = true;
|
||||
|
||||
List<String>? preSelectedEmojies;
|
||||
ThemeMode? themeMode;
|
||||
|
||||
Map<String, List<String>>? autoDownloadOptions;
|
||||
bool? storeMediaFilesInGallery;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool storeMediaFilesInGallery = false;
|
||||
|
||||
List<String>? lastUsedEditorEmojis;
|
||||
|
||||
String? lastPlanBallance;
|
||||
String? additionalUserInvites;
|
||||
|
||||
DateTime? lastImageSend;
|
||||
int? todaysImageCounter;
|
||||
List<String>? tutorialDisplayed;
|
||||
|
||||
int? myBestFriendContactId;
|
||||
|
|
@ -50,22 +64,35 @@ class UserData {
|
|||
|
||||
// --- BACKUP ---
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool identityBackupEnabled = false;
|
||||
DateTime? identityBackupLastBackupTime;
|
||||
|
||||
@JsonKey(defaultValue: 0)
|
||||
int identityBackupLastBackupSize = 0;
|
||||
DateTime? nextTimeToShowBackupNotice;
|
||||
BackupServer? backupServer;
|
||||
List<int>? twonlySafeEncryptionKey;
|
||||
List<int>? twonlySafeBackupId;
|
||||
TwonlySafeBackup? twonlySafeBackup;
|
||||
|
||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
enum LastBackupUploadState { none, pending, failed, success }
|
||||
|
||||
@JsonSerializable()
|
||||
class TwonlySafeBackup {
|
||||
TwonlySafeBackup({
|
||||
required this.backupId,
|
||||
required this.encryptionKey,
|
||||
});
|
||||
|
||||
int lastBackupSize = 0;
|
||||
LastBackupUploadState backupUploadState = LastBackupUploadState.none;
|
||||
DateTime? lastBackupDone;
|
||||
List<int> backupId;
|
||||
List<int> encryptionKey;
|
||||
|
||||
factory TwonlySafeBackup.fromJson(Map<String, dynamic> json) =>
|
||||
_$TwonlySafeBackupFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BackupServer {
|
||||
BackupServer({
|
||||
|
|
|
|||
|
|
@ -15,28 +15,31 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
)
|
||||
..avatarSvg = json['avatarSvg'] as String?
|
||||
..avatarJson = json['avatarJson'] as String?
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt()
|
||||
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
|
||||
..lastImageSend = json['lastImageSend'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastImageSend'] as String)
|
||||
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
|
||||
..themeMode =
|
||||
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
|
||||
ThemeMode.system
|
||||
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
|
||||
..useHighQuality = json['useHighQuality'] as bool?
|
||||
..useHighQuality = json['useHighQuality'] as bool? ?? true
|
||||
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList()
|
||||
..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'])
|
||||
..autoDownloadOptions =
|
||||
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) =>
|
||||
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()),
|
||||
)
|
||||
..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool?
|
||||
..storeMediaFilesInGallery =
|
||||
json['storeMediaFilesInGallery'] as bool? ?? false
|
||||
..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList()
|
||||
..lastPlanBallance = json['lastPlanBallance'] as String?
|
||||
..additionalUserInvites = json['additionalUserInvites'] as String?
|
||||
..lastImageSend = json['lastImageSend'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastImageSend'] as String)
|
||||
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
|
||||
..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList()
|
||||
|
|
@ -45,26 +48,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
json['signalLastSignedPreKeyUpdated'] == null
|
||||
? null
|
||||
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
|
||||
..identityBackupEnabled = json['identityBackupEnabled'] as bool? ?? false
|
||||
..identityBackupLastBackupTime =
|
||||
json['identityBackupLastBackupTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['identityBackupLastBackupTime'] as String)
|
||||
..identityBackupLastBackupSize =
|
||||
(json['identityBackupLastBackupSize'] as num?)?.toInt() ?? 0
|
||||
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
|
||||
? null
|
||||
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
|
||||
..backupServer = json['backupServer'] == null
|
||||
? null
|
||||
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
|
||||
..twonlySafeEncryptionKey =
|
||||
(json['twonlySafeEncryptionKey'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..twonlySafeBackupId = (json['twonlySafeBackupId'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList();
|
||||
..twonlySafeBackup = json['twonlySafeBackup'] == null
|
||||
? null
|
||||
: TwonlySafeBackup.fromJson(
|
||||
json['twonlySafeBackup'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
|
|
@ -74,31 +67,26 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
'todaysImageCounter': instance.todaysImageCounter,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'useHighQuality': instance.useHighQuality,
|
||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||
'themeMode': _$ThemeModeEnumMap[instance.themeMode],
|
||||
'autoDownloadOptions': instance.autoDownloadOptions,
|
||||
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
|
||||
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
|
||||
'lastPlanBallance': instance.lastPlanBallance,
|
||||
'additionalUserInvites': instance.additionalUserInvites,
|
||||
'lastImageSend': instance.lastImageSend?.toIso8601String(),
|
||||
'todaysImageCounter': instance.todaysImageCounter,
|
||||
'tutorialDisplayed': instance.tutorialDisplayed,
|
||||
'myBestFriendContactId': instance.myBestFriendContactId,
|
||||
'signalLastSignedPreKeyUpdated':
|
||||
instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
|
||||
'identityBackupEnabled': instance.identityBackupEnabled,
|
||||
'identityBackupLastBackupTime':
|
||||
instance.identityBackupLastBackupTime?.toIso8601String(),
|
||||
'identityBackupLastBackupSize': instance.identityBackupLastBackupSize,
|
||||
'nextTimeToShowBackupNotice':
|
||||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeEncryptionKey': instance.twonlySafeEncryptionKey,
|
||||
'twonlySafeBackupId': instance.twonlySafeBackupId,
|
||||
'twonlySafeBackup': instance.twonlySafeBackup,
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
|
@ -107,6 +95,39 @@ const _$ThemeModeEnumMap = {
|
|||
ThemeMode.dark: 'dark',
|
||||
};
|
||||
|
||||
TwonlySafeBackup _$TwonlySafeBackupFromJson(Map<String, dynamic> json) =>
|
||||
TwonlySafeBackup(
|
||||
backupId: (json['backupId'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
encryptionKey: (json['encryptionKey'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
)
|
||||
..lastBackupSize = (json['lastBackupSize'] as num).toInt()
|
||||
..backupUploadState =
|
||||
$enumDecode(_$LastBackupUploadStateEnumMap, json['backupUploadState'])
|
||||
..lastBackupDone = json['lastBackupDone'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastBackupDone'] as String);
|
||||
|
||||
Map<String, dynamic> _$TwonlySafeBackupToJson(TwonlySafeBackup instance) =>
|
||||
<String, dynamic>{
|
||||
'lastBackupSize': instance.lastBackupSize,
|
||||
'backupUploadState':
|
||||
_$LastBackupUploadStateEnumMap[instance.backupUploadState]!,
|
||||
'lastBackupDone': instance.lastBackupDone?.toIso8601String(),
|
||||
'backupId': instance.backupId,
|
||||
'encryptionKey': instance.encryptionKey,
|
||||
};
|
||||
|
||||
const _$LastBackupUploadStateEnumMap = {
|
||||
LastBackupUploadState.none: 'none',
|
||||
LastBackupUploadState.pending: 'pending',
|
||||
LastBackupUploadState.failed: 'failed',
|
||||
LastBackupUploadState.success: 'success',
|
||||
};
|
||||
|
||||
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
|
||||
serverUrl: json['serverUrl'] as String,
|
||||
retentionDays: (json['retentionDays'] as num).toInt(),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
static const ErrorCode PlanUpgradeNotYearly = ErrorCode._(1026, _omitEnumNames ? '' : 'PlanUpgradeNotYearly');
|
||||
static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey');
|
||||
static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound');
|
||||
static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken');
|
||||
|
||||
static const $core.List<ErrorCode> values = <ErrorCode> [
|
||||
Unknown,
|
||||
|
|
@ -78,6 +79,7 @@ class ErrorCode extends $pb.ProtobufEnum {
|
|||
PlanUpgradeNotYearly,
|
||||
InvalidSignedPreKey,
|
||||
UserIdNotFound,
|
||||
UserIdAlreadyTaken,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ const ErrorCode$json = {
|
|||
{'1': 'PlanUpgradeNotYearly', '2': 1026},
|
||||
{'1': 'InvalidSignedPreKey', '2': 1027},
|
||||
{'1': 'UserIdNotFound', '2': 1028},
|
||||
{'1': 'UserIdAlreadyTaken', '2': 1029},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -66,5 +67,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
|
|||
'Cg5JbnZhbGlkUHJlS2V5cxD8BxITCg5Wb3VjaGVySW5WYWxpZBD9BxITCg5QbGFuTm90QWxsb3'
|
||||
'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q'
|
||||
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
|
||||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAg=');
|
||||
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
|
||||
'EIUI');
|
||||
|
||||
|
|
|
|||
160
lib/src/model/protobuf/backup/backup.pb.dart
Normal file
160
lib/src/model/protobuf/backup/backup.pb.dart
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: backup.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class TwonlySafeBackupContent extends $pb.GeneratedMessage {
|
||||
factory TwonlySafeBackupContent({
|
||||
$core.String? secureStorageJson,
|
||||
$core.List<$core.int>? twonlyDatabase,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (secureStorageJson != null) {
|
||||
$result.secureStorageJson = secureStorageJson;
|
||||
}
|
||||
if (twonlyDatabase != null) {
|
||||
$result.twonlyDatabase = twonlyDatabase;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
TwonlySafeBackupContent._() : super();
|
||||
factory TwonlySafeBackupContent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory TwonlySafeBackupContent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TwonlySafeBackupContent', createEmptyInstance: create)
|
||||
..aOS(1, _omitFieldNames ? '' : 'secureStorageJson', protoName: 'secureStorageJson')
|
||||
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'twonlyDatabase', $pb.PbFieldType.OY, protoName: 'twonlyDatabase')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
TwonlySafeBackupContent clone() => TwonlySafeBackupContent()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
TwonlySafeBackupContent copyWith(void Function(TwonlySafeBackupContent) updates) => super.copyWith((message) => updates(message as TwonlySafeBackupContent)) as TwonlySafeBackupContent;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TwonlySafeBackupContent create() => TwonlySafeBackupContent._();
|
||||
TwonlySafeBackupContent createEmptyInstance() => create();
|
||||
static $pb.PbList<TwonlySafeBackupContent> createRepeated() => $pb.PbList<TwonlySafeBackupContent>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TwonlySafeBackupContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TwonlySafeBackupContent>(create);
|
||||
static TwonlySafeBackupContent? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.String get secureStorageJson => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set secureStorageJson($core.String v) { $_setString(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasSecureStorageJson() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearSecureStorageJson() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get twonlyDatabase => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set twonlyDatabase($core.List<$core.int> v) { $_setBytes(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasTwonlyDatabase() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearTwonlyDatabase() => clearField(2);
|
||||
}
|
||||
|
||||
class TwonlySafeBackupEncrypted extends $pb.GeneratedMessage {
|
||||
factory TwonlySafeBackupEncrypted({
|
||||
$core.List<$core.int>? mac,
|
||||
$core.List<$core.int>? nonce,
|
||||
$core.List<$core.int>? cipherText,
|
||||
}) {
|
||||
final $result = create();
|
||||
if (mac != null) {
|
||||
$result.mac = mac;
|
||||
}
|
||||
if (nonce != null) {
|
||||
$result.nonce = nonce;
|
||||
}
|
||||
if (cipherText != null) {
|
||||
$result.cipherText = cipherText;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
TwonlySafeBackupEncrypted._() : super();
|
||||
factory TwonlySafeBackupEncrypted.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
|
||||
factory TwonlySafeBackupEncrypted.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
|
||||
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TwonlySafeBackupEncrypted', createEmptyInstance: create)
|
||||
..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'nonce', $pb.PbFieldType.OY)
|
||||
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'cipherText', $pb.PbFieldType.OY, protoName: 'cipherText')
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
|
||||
'Will be removed in next major version')
|
||||
TwonlySafeBackupEncrypted clone() => TwonlySafeBackupEncrypted()..mergeFromMessage(this);
|
||||
@$core.Deprecated(
|
||||
'Using this can add significant overhead to your binary. '
|
||||
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
|
||||
'Will be removed in next major version')
|
||||
TwonlySafeBackupEncrypted copyWith(void Function(TwonlySafeBackupEncrypted) updates) => super.copyWith((message) => updates(message as TwonlySafeBackupEncrypted)) as TwonlySafeBackupEncrypted;
|
||||
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TwonlySafeBackupEncrypted create() => TwonlySafeBackupEncrypted._();
|
||||
TwonlySafeBackupEncrypted createEmptyInstance() => create();
|
||||
static $pb.PbList<TwonlySafeBackupEncrypted> createRepeated() => $pb.PbList<TwonlySafeBackupEncrypted>();
|
||||
@$core.pragma('dart2js:noInline')
|
||||
static TwonlySafeBackupEncrypted getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<TwonlySafeBackupEncrypted>(create);
|
||||
static TwonlySafeBackupEncrypted? _defaultInstance;
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$core.List<$core.int> get mac => $_getN(0);
|
||||
@$pb.TagNumber(1)
|
||||
set mac($core.List<$core.int> v) { $_setBytes(0, v); }
|
||||
@$pb.TagNumber(1)
|
||||
$core.bool hasMac() => $_has(0);
|
||||
@$pb.TagNumber(1)
|
||||
void clearMac() => clearField(1);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
$core.List<$core.int> get nonce => $_getN(1);
|
||||
@$pb.TagNumber(2)
|
||||
set nonce($core.List<$core.int> v) { $_setBytes(1, v); }
|
||||
@$pb.TagNumber(2)
|
||||
$core.bool hasNonce() => $_has(1);
|
||||
@$pb.TagNumber(2)
|
||||
void clearNonce() => clearField(2);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
$core.List<$core.int> get cipherText => $_getN(2);
|
||||
@$pb.TagNumber(3)
|
||||
set cipherText($core.List<$core.int> v) { $_setBytes(2, v); }
|
||||
@$pb.TagNumber(3)
|
||||
$core.bool hasCipherText() => $_has(2);
|
||||
@$pb.TagNumber(3)
|
||||
void clearCipherText() => clearField(3);
|
||||
}
|
||||
|
||||
|
||||
const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names');
|
||||
const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names');
|
||||
11
lib/src/model/protobuf/backup/backup.pbenum.dart
Normal file
11
lib/src/model/protobuf/backup/backup.pbenum.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: backup.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
44
lib/src/model/protobuf/backup/backup.pbjson.dart
Normal file
44
lib/src/model/protobuf/backup/backup.pbjson.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: backup.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
import 'dart:convert' as $convert;
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use twonlySafeBackupContentDescriptor instead')
|
||||
const TwonlySafeBackupContent$json = {
|
||||
'1': 'TwonlySafeBackupContent',
|
||||
'2': [
|
||||
{'1': 'secureStorageJson', '3': 1, '4': 1, '5': 9, '10': 'secureStorageJson'},
|
||||
{'1': 'twonlyDatabase', '3': 2, '4': 1, '5': 12, '10': 'twonlyDatabase'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `TwonlySafeBackupContent`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List twonlySafeBackupContentDescriptor = $convert.base64Decode(
|
||||
'ChdUd29ubHlTYWZlQmFja3VwQ29udGVudBIsChFzZWN1cmVTdG9yYWdlSnNvbhgBIAEoCVIRc2'
|
||||
'VjdXJlU3RvcmFnZUpzb24SJgoOdHdvbmx5RGF0YWJhc2UYAiABKAxSDnR3b25seURhdGFiYXNl');
|
||||
|
||||
@$core.Deprecated('Use twonlySafeBackupEncryptedDescriptor instead')
|
||||
const TwonlySafeBackupEncrypted$json = {
|
||||
'1': 'TwonlySafeBackupEncrypted',
|
||||
'2': [
|
||||
{'1': 'mac', '3': 1, '4': 1, '5': 12, '10': 'mac'},
|
||||
{'1': 'nonce', '3': 2, '4': 1, '5': 12, '10': 'nonce'},
|
||||
{'1': 'cipherText', '3': 3, '4': 1, '5': 12, '10': 'cipherText'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `TwonlySafeBackupEncrypted`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List twonlySafeBackupEncryptedDescriptor = $convert.base64Decode(
|
||||
'ChlUd29ubHlTYWZlQmFja3VwRW5jcnlwdGVkEhAKA21hYxgBIAEoDFIDbWFjEhQKBW5vbmNlGA'
|
||||
'IgASgMUgVub25jZRIeCgpjaXBoZXJUZXh0GAMgASgMUgpjaXBoZXJUZXh0');
|
||||
|
||||
14
lib/src/model/protobuf/backup/backup.pbserver.dart
Normal file
14
lib/src/model/protobuf/backup/backup.pbserver.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Generated code. Do not modify.
|
||||
// source: backup.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'backup.pb.dart';
|
||||
|
||||
12
lib/src/model/protobuf/backup/backup.proto
Normal file
12
lib/src/model/protobuf/backup/backup.proto
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
syntax = "proto3";
|
||||
|
||||
message TwonlySafeBackupContent {
|
||||
string secureStorageJson = 1;
|
||||
bytes twonlyDatabase = 2;
|
||||
}
|
||||
|
||||
message TwonlySafeBackupEncrypted {
|
||||
bytes mac = 1;
|
||||
bytes nonce = 2;
|
||||
bytes cipherText = 3;
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import 'package:twonly/src/model/json/message.dart';
|
|||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_received.dart';
|
||||
import 'package:twonly/src/services/backup.identitiy.service.dart';
|
||||
import 'package:twonly/src/services/notification.service.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -67,6 +68,9 @@ Future initFileDownloader() async {
|
|||
if (update.task.taskId.contains("download_")) {
|
||||
await handleDownloadStatusUpdate(update);
|
||||
}
|
||||
if (update.task.taskId.contains("backup")) {
|
||||
await handleBackupStatusUpdate(update);
|
||||
}
|
||||
case TaskProgressUpdate():
|
||||
Log.info(
|
||||
'Progress update for ${update.task} with progress ${update.progress}');
|
||||
|
|
|
|||
|
|
@ -230,13 +230,12 @@ Future notifyContactsAboutProfileChange() async {
|
|||
|
||||
UserData? user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.avatarCounter == null) return;
|
||||
if (user.avatarSvg == null) return;
|
||||
|
||||
for (Contact contact in contacts) {
|
||||
if (contact.myAvatarCounter < user.avatarCounter!) {
|
||||
if (contact.myAvatarCounter < user.avatarCounter) {
|
||||
twonlyDB.contactsDao.updateContact(contact.userId,
|
||||
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter!)));
|
||||
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter)));
|
||||
await encryptAndSendMessageAsync(
|
||||
null,
|
||||
contact.userId,
|
||||
|
|
|
|||
|
|
@ -1,34 +1,50 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hashlib/hashlib.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly_database.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_send.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
||||
Future performTwonlySafeBackup() async {
|
||||
Future performTwonlySafeBackup({bool force = false}) async {
|
||||
Log.info("Starting new backup creation.");
|
||||
final user = await getUser();
|
||||
|
||||
if (user == null || user.twonlySafeBackup == null) {
|
||||
Log.warn("perform twonly safe backup was called while it is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.twonlySafeBackup!.backupUploadState ==
|
||||
LastBackupUploadState.pending) {
|
||||
Log.warn("Backup upload is already pending.");
|
||||
return;
|
||||
}
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
|
||||
var originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||
var backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
|
||||
if (backupDir.existsSync()) {
|
||||
await backupDir.delete(recursive: true);
|
||||
}
|
||||
final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
|
||||
await backupDir.create(recursive: true);
|
||||
|
||||
var backupDatabaseFile =
|
||||
final backupDatabaseFile =
|
||||
File(join(backupDir.path, "twonly_database.backup.sqlite"));
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
|
||||
await originalDatabase.copy(backupDatabaseFile.path);
|
||||
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
final backupDB = TwonlyDatabase(
|
||||
driftDatabase(
|
||||
name: "twonly_database.backup",
|
||||
|
|
@ -48,30 +64,145 @@ Future performTwonlySafeBackup() async {
|
|||
await storage.read(key: SecureStorageKeys.signalIdentity);
|
||||
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
|
||||
await storage.read(key: SecureStorageKeys.signalSignedPreKey);
|
||||
secureStorageBackup[SecureStorageKeys.userData] =
|
||||
await storage.read(key: SecureStorageKeys.userData);
|
||||
|
||||
var backupSecureStorage = File(join(backupDir.path, "secure_storage.json"));
|
||||
var userBackup = await getUser();
|
||||
if (userBackup == null) return;
|
||||
// FILTER settings which should not be in the backup
|
||||
userBackup.twonlySafeBackup = null;
|
||||
|
||||
await backupSecureStorage.writeAsString(jsonEncode(secureStorageBackup));
|
||||
secureStorageBackup[SecureStorageKeys.userData] = jsonEncode(userBackup);
|
||||
|
||||
// Compress and convert backup data
|
||||
|
||||
final twonlyDatabaseBytes = await backupDatabaseFile.readAsBytes();
|
||||
await backupDatabaseFile.delete();
|
||||
|
||||
final backupProto = TwonlySafeBackupContent(
|
||||
secureStorageJson: jsonEncode(secureStorageBackup),
|
||||
twonlyDatabase: twonlyDatabaseBytes,
|
||||
);
|
||||
|
||||
final backupBytes = gzip.encode(backupProto.writeToBuffer());
|
||||
|
||||
final backupHash = uint8ListToHex((await Sha256().hash(backupBytes)).bytes);
|
||||
|
||||
if (user.twonlySafeBackup!.lastBackupDone == null ||
|
||||
user.twonlySafeBackup!.lastBackupDone!
|
||||
.isAfter(DateTime.now().subtract(Duration(days: 90)))) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
final lastHash =
|
||||
await storage.read(key: SecureStorageKeys.twonlySafeLastBackupHash);
|
||||
|
||||
if (lastHash != null && !force) {
|
||||
if (backupHash == lastHash) {
|
||||
Log.info("Since last backup nothing has changed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
await storage.write(
|
||||
key: SecureStorageKeys.twonlySafeLastBackupHash,
|
||||
value: backupHash,
|
||||
);
|
||||
|
||||
// Encrypt backup data
|
||||
|
||||
final xchacha20 = Xchacha20.poly1305Aead();
|
||||
final nonce = xchacha20.newNonce();
|
||||
|
||||
final secretBox = await xchacha20.encrypt(
|
||||
backupBytes,
|
||||
secretKey: SecretKey(user.twonlySafeBackup!.encryptionKey),
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
final encryptedBackupBytes = (TwonlySafeBackupEncrypted(
|
||||
mac: secretBox.mac.bytes,
|
||||
nonce: nonce,
|
||||
cipherText: secretBox.cipherText,
|
||||
)).writeToBuffer();
|
||||
|
||||
Log.info("Backup files created.");
|
||||
|
||||
var twonlySafeBackupZip = File(join(backupDir.path, "twonly_safe.zip"));
|
||||
var encryptedBackupBytesFile =
|
||||
File(join(backupDir.path, "twonly_safe.backup"));
|
||||
|
||||
await createZipArchive(
|
||||
twonlySafeBackupZip.path, [backupSecureStorage, backupDatabaseFile]);
|
||||
await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
|
||||
|
||||
// await backupDir.delete(recursive: true);
|
||||
Log.info(
|
||||
"Create twonly Safe backup with a size of ${encryptedBackupBytes.length} bytes.");
|
||||
|
||||
String backupServerUrl = "https://safe.twonly.eu/";
|
||||
|
||||
if (user.backupServer != null) {
|
||||
backupServerUrl = user.backupServer!.serverUrl;
|
||||
|
||||
if (encryptedBackupBytes.length > user.backupServer!.maxBackupBytes) {
|
||||
Log.error("Backup is to big for the alternative backup server.");
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
return user;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String backupId =
|
||||
uint8ListToHex(user.twonlySafeBackup!.backupId).toLowerCase();
|
||||
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: "backup",
|
||||
file: encryptedBackupBytesFile,
|
||||
httpRequestMethod: "PUT",
|
||||
url: "${backupServerUrl}backups/$backupId",
|
||||
requiresWiFi: true,
|
||||
priority: 5,
|
||||
retries: 2,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
);
|
||||
if (await FileDownloader().enqueue(task)) {
|
||||
Log.info("Starting upload from twonly Safe backup.");
|
||||
await updateUserdata((user) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.pending;
|
||||
user.twonlySafeBackup!.lastBackupDone = DateTime.now();
|
||||
user.twonlySafeBackup!.lastBackupSize = encryptedBackupBytes.length;
|
||||
return user;
|
||||
});
|
||||
gUpdateBackupView();
|
||||
} else {
|
||||
Log.error("Error starting UploadTask for twonly Safe.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createZipArchive(String zipFilePath, List<File> filesToZip) async {
|
||||
final encoder = ZipFileEncoder();
|
||||
encoder.create(zipFilePath);
|
||||
for (var file in filesToZip) {
|
||||
await encoder.addFile(file);
|
||||
Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error(
|
||||
"twonly Safe upload failed. ${update.responseStatusCode} ${update.responseBody} ${update.responseHeaders} ${update.exception}");
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState = LastBackupUploadState.failed;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
Log.error(
|
||||
"twonly Safe uploaded with status code ${update.responseStatusCode}");
|
||||
await updateUserdata((user) {
|
||||
if (user.twonlySafeBackup != null) {
|
||||
user.twonlySafeBackup!.backupUploadState =
|
||||
LastBackupUploadState.success;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
} else {
|
||||
Log.info("Backup is in state: ${update.status}");
|
||||
return;
|
||||
}
|
||||
await encoder.close();
|
||||
gUpdateBackupView();
|
||||
}
|
||||
|
||||
Future enableTwonlySafe(String password) async {
|
||||
|
|
@ -81,9 +212,10 @@ Future enableTwonlySafe(String password) async {
|
|||
final (backupId, encryptionKey) = await getMasterKey(password, user.username);
|
||||
|
||||
await updateUserdata((user) {
|
||||
user.identityBackupEnabled = true;
|
||||
user.twonlySafeBackupId = backupId.toList();
|
||||
user.twonlySafeEncryptionKey = encryptionKey.toList();
|
||||
user.twonlySafeBackup = TwonlySafeBackup(
|
||||
encryptionKey: encryptionKey,
|
||||
backupId: backupId,
|
||||
);
|
||||
return user;
|
||||
});
|
||||
startTwonlySafeBackup();
|
||||
|
|
@ -92,11 +224,7 @@ Future enableTwonlySafe(String password) async {
|
|||
|
||||
Future disableTwonlySafe() async {
|
||||
await updateUserdata((user) {
|
||||
user.identityBackupEnabled = false;
|
||||
user.twonlySafeBackupId = null;
|
||||
user.twonlySafeEncryptionKey = null;
|
||||
user.identityBackupLastBackupTime = null;
|
||||
user.identityBackupLastBackupSize = 0;
|
||||
user.twonlySafeBackup = null;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -384,3 +385,31 @@ List<List<dynamic>> chatMessages = [
|
|||
],
|
||||
["Curabitur blandit tempus porttitor.", DateTime.now()],
|
||||
];
|
||||
|
||||
String formatDateTime(BuildContext context, DateTime? dateTime) {
|
||||
if (dateTime == null) {
|
||||
return "Never";
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
final date = DateFormat.yMd(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(dateTime);
|
||||
|
||||
final time = DateFormat.Hm(Localizations.localeOf(context).toLanguageTag())
|
||||
.format(dateTime);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return time;
|
||||
} else {
|
||||
return "$time $date";
|
||||
}
|
||||
}
|
||||
|
||||
String formatBytes(int bytes, {int decimalPlaces = 2}) {
|
||||
if (bytes <= 0) return "0 Bytes";
|
||||
const List<String> units = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
final int unitIndex = (log(bytes) / log(1000)).floor();
|
||||
final double formattedSize = bytes / pow(1000, unitIndex);
|
||||
return "${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
memoryPath = join(memoryPath, token);
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user != null && (user.storeMediaFilesInGallery ?? true)) {}
|
||||
bool storeToGallery = user?.storeMediaFilesInGallery ?? true;
|
||||
if (user != null) return;
|
||||
bool storeToGallery = user!.storeMediaFilesInGallery;
|
||||
|
||||
if (widget.videoFilePath != null) {
|
||||
memoryPath += ".mp4";
|
||||
|
|
|
|||
|
|
@ -176,9 +176,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
void initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
if (user.useHighQuality != null) {
|
||||
useHighQuality = user.useHighQuality!;
|
||||
}
|
||||
useHighQuality = user.useHighQuality;
|
||||
hasAudioPermission = await Permission.microphone.isGranted;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
|||
if (user != null &&
|
||||
(user.nextTimeToShowBackupNotice == null ||
|
||||
DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) {
|
||||
if (!gIsDemoUser && (!user.identityBackupEnabled)) {
|
||||
if (!gIsDemoUser && (user.twonlySafeBackup == null)) {
|
||||
showBackupNotice = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
imageSaved = true;
|
||||
});
|
||||
final user = await getUser();
|
||||
if (user != null && (user.storeMediaFilesInGallery ?? true)) {
|
||||
if (user != null && (user.storeMediaFilesInGallery)) {
|
||||
if (videoPath != null) {
|
||||
await saveVideoToGallery(videoPath!);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import 'package:twonly/globals.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/signal/identity.signal.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
|
@ -43,6 +45,11 @@ class _RegisterViewState extends State<RegisterView> {
|
|||
Log.info("Got user_id ${res.value} from server");
|
||||
userId = res.value.userid.toInt();
|
||||
} else {
|
||||
if (res.error == ErrorCode.UserIdAlreadyTaken) {
|
||||
Log.error("User ID already token. Tying again.");
|
||||
await deleteLocalUserData();
|
||||
return createNewUser();
|
||||
}
|
||||
if (mounted) {
|
||||
showAlertDialog(
|
||||
context,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/backup.identitiy.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
|
||||
|
||||
Function() gUpdateBackupView = () {};
|
||||
|
||||
class BackupView extends StatefulWidget {
|
||||
const BackupView({super.key});
|
||||
|
||||
|
|
@ -12,25 +16,54 @@ class BackupView extends StatefulWidget {
|
|||
State<BackupView> createState() => _BackupViewState();
|
||||
}
|
||||
|
||||
BackupServer defaultBackupServer = BackupServer(
|
||||
serverUrl: "Default",
|
||||
retentionDays: 180,
|
||||
maxBackupBytes: 2097152,
|
||||
);
|
||||
|
||||
class _BackupViewState extends State<BackupView> {
|
||||
bool _twonlyIdBackupEnabled = false;
|
||||
DateTime? _twonlyIdLastBackup;
|
||||
bool _dataBackupEnabled = false;
|
||||
DateTime? _dataBackupLastBackup;
|
||||
TwonlySafeBackup? twonlySafeBackup;
|
||||
BackupServer backupServer = defaultBackupServer;
|
||||
|
||||
int activePageIdx = 0;
|
||||
|
||||
final PageController pageController =
|
||||
PageController(keepPage: true, initialPage: 0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
gUpdateBackupView = initAsync;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
gUpdateBackupView = () {};
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user != null) {
|
||||
_twonlyIdBackupEnabled = user.identityBackupEnabled;
|
||||
_twonlyIdLastBackup = user.identityBackupLastBackupTime;
|
||||
_dataBackupEnabled = false;
|
||||
setState(() {});
|
||||
twonlySafeBackup = user?.twonlySafeBackup;
|
||||
backupServer = defaultBackupServer;
|
||||
if (user?.backupServer != null) {
|
||||
backupServer = user!.backupServer!;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String backupStatus(LastBackupUploadState status) {
|
||||
switch (status) {
|
||||
case LastBackupUploadState.none:
|
||||
return '-';
|
||||
case LastBackupUploadState.pending:
|
||||
return 'Pending';
|
||||
case LastBackupUploadState.failed:
|
||||
return 'Failed';
|
||||
case LastBackupUploadState.success:
|
||||
return 'Success';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,16 +73,73 @@ class _BackupViewState extends State<BackupView> {
|
|||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsBackup),
|
||||
),
|
||||
body: ListView(
|
||||
body: PageView(
|
||||
controller: pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
activePageIdx = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
BackupOption(
|
||||
title: 'twonly Safe',
|
||||
description:
|
||||
'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.',
|
||||
lastBackup: _twonlyIdLastBackup,
|
||||
autoBackupEnabled: _twonlyIdBackupEnabled,
|
||||
autoBackupEnabled: twonlySafeBackup != null,
|
||||
child: (twonlySafeBackup != null)
|
||||
? Table(
|
||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
...[
|
||||
(
|
||||
"Server",
|
||||
(backupServer.serverUrl.contains("@"))
|
||||
? backupServer.serverUrl.split("@")[1]
|
||||
: backupServer.serverUrl
|
||||
.replaceAll("https://", "")
|
||||
),
|
||||
(
|
||||
"Max. Backup-Größe",
|
||||
formatBytes(backupServer.maxBackupBytes)
|
||||
),
|
||||
("Speicherdauer", "${backupServer.retentionDays} Days"),
|
||||
(
|
||||
"Letztes Backup",
|
||||
formatDateTime(
|
||||
context, twonlySafeBackup!.lastBackupDone)
|
||||
),
|
||||
(
|
||||
"Backup-Größe",
|
||||
formatBytes(twonlySafeBackup!.lastBackupSize)
|
||||
),
|
||||
(
|
||||
"Ergebnis",
|
||||
backupStatus(twonlySafeBackup!.backupUploadState)
|
||||
)
|
||||
].map((pair) {
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
// padding: EdgeInsets.all(4),
|
||||
child: Text(pair.$1),
|
||||
),
|
||||
TableCell(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
pair.$2,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (_twonlyIdBackupEnabled) {
|
||||
if (twonlySafeBackup != null) {
|
||||
bool disable = await showAlertDialog(context, "Are you sure?",
|
||||
"Without an backup, you can not restore your user account.");
|
||||
if (disable) {
|
||||
|
|
@ -68,12 +158,41 @@ class _BackupViewState extends State<BackupView> {
|
|||
title: 'Daten-Backup (Coming Soon)',
|
||||
description:
|
||||
'This backup contains besides of your twonly-Identity also all of your media files. This backup will also be encrypted using a password chosen by the user but stored locally on the smartphone. You then have to ensure to manually copy it onto your laptop or device of your choice.',
|
||||
autoBackupEnabled: _dataBackupEnabled,
|
||||
onTap: () {},
|
||||
lastBackup: _dataBackupLastBackup,
|
||||
autoBackupEnabled: false,
|
||||
onTap: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
showSelectedLabels: true,
|
||||
showUnselectedLabels: true,
|
||||
unselectedIconTheme: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.inverseSurface.withAlpha(150)),
|
||||
selectedIconTheme:
|
||||
IconThemeData(color: Theme.of(context).colorScheme.inverseSurface),
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.vault, size: 17),
|
||||
label: "twonly Safe",
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.boxArchive, size: 17),
|
||||
label: "Daten-Backup",
|
||||
),
|
||||
],
|
||||
onTap: (int index) {
|
||||
activePageIdx = index;
|
||||
setState(() {
|
||||
pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.bounceIn,
|
||||
);
|
||||
});
|
||||
},
|
||||
currentIndex: activePageIdx,
|
||||
// ),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -83,41 +202,23 @@ class BackupOption extends StatelessWidget {
|
|||
final String description;
|
||||
final Widget? child;
|
||||
final bool autoBackupEnabled;
|
||||
final DateTime? lastBackup;
|
||||
final Function() onTap;
|
||||
final Function()? onTap;
|
||||
|
||||
const BackupOption({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.autoBackupEnabled,
|
||||
required this.lastBackup,
|
||||
required this.onTap,
|
||||
this.child,
|
||||
});
|
||||
|
||||
String formatDateTime(DateTime? dateTime) {
|
||||
if (dateTime == null) {
|
||||
return "Never";
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Today';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return '${difference.inDays} Days ago';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: (autoBackupEnabled) ? null : onTap,
|
||||
child: Card(
|
||||
margin: EdgeInsets.all(8.0),
|
||||
margin: EdgeInsets.all(16.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
|
|
@ -131,21 +232,18 @@ class BackupOption extends StatelessWidget {
|
|||
Text(description),
|
||||
SizedBox(height: 8.0),
|
||||
(child != null) ? child! : Container(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Last backup: ${formatDateTime(lastBackup)}'),
|
||||
(autoBackupEnabled)
|
||||
? OutlinedButton(
|
||||
onPressed: onTap,
|
||||
child: Text("Disable"),
|
||||
)
|
||||
: FilledButton(
|
||||
onPressed: onTap,
|
||||
child: Text("Enable"),
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Center(
|
||||
child: (autoBackupEnabled)
|
||||
? OutlinedButton(
|
||||
onPressed: onTap,
|
||||
child: Text("Disable"),
|
||||
)
|
||||
: FilledButton(
|
||||
onPressed: onTap,
|
||||
child: Text("Enable"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
|
|||
setState(() {
|
||||
autoDownloadOptions =
|
||||
user.autoDownloadOptions ?? defaultAutoDownloadOptions;
|
||||
storeMediaFilesInGallery = user.storeMediaFilesInGallery ?? true;
|
||||
storeMediaFilesInGallery = user.storeMediaFilesInGallery;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,7 @@ class ModifyAvatar extends StatelessWidget {
|
|||
await updateUserdata((user) {
|
||||
user.avatarJson = json;
|
||||
user.avatarSvg = svg;
|
||||
if (user.avatarCounter == null) {
|
||||
user.avatarCounter = 1;
|
||||
} else {
|
||||
user.avatarCounter = user.avatarCounter! + 1;
|
||||
}
|
||||
user.avatarCounter = user.avatarCounter + 1;
|
||||
return user;
|
||||
});
|
||||
await notifyContactsAboutProfileChange();
|
||||
|
|
|
|||
|
|
@ -32,11 +32,7 @@ class _ProfileViewState extends State<ProfileView> {
|
|||
Future updateUserDisplayName(String displayName) async {
|
||||
await updateUserdata((user) {
|
||||
user.displayName = displayName;
|
||||
if (user.avatarCounter == null) {
|
||||
user.avatarCounter = 1;
|
||||
} else {
|
||||
user.avatarCounter = user.avatarCounter! + 1;
|
||||
}
|
||||
user.avatarCounter = user.avatarCounter + 1;
|
||||
return user;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@ dependencies:
|
|||
tutorial_coach_mark: ^1.3.0
|
||||
background_downloader: ^9.2.2
|
||||
hashlib: ^2.0.0
|
||||
archive: ^4.0.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in a new issue