diff --git a/generate_proto.sh b/generate_proto.sh index 6cfa149..dd2ef76 100755 --- a/generate_proto.sh +++ b/generate_proto.sh @@ -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/" diff --git a/lib/src/constants/secure_storage_keys.dart b/lib/src/constants/secure_storage_keys.dart index e0c6431..14035c0 100644 --- a/lib/src/constants/secure_storage_keys.dart +++ b/lib/src/constants/secure_storage_keys.dart @@ -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"; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 7e1c5a5..0362d94 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -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? preSelectedEmojies; - ThemeMode? themeMode; + Map>? autoDownloadOptions; - bool? storeMediaFilesInGallery; + + @JsonKey(defaultValue: false) + bool storeMediaFilesInGallery = false; + List? lastUsedEditorEmojis; String? lastPlanBallance; String? additionalUserInvites; - DateTime? lastImageSend; - int? todaysImageCounter; List? 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? twonlySafeEncryptionKey; - List? twonlySafeBackupId; + TwonlySafeBackup? twonlySafeBackup; factory UserData.fromJson(Map json) => _$UserDataFromJson(json); Map 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 backupId; + List encryptionKey; + + factory TwonlySafeBackup.fromJson(Map json) => + _$TwonlySafeBackupFromJson(json); + Map toJson() => _$TwonlySafeBackupToJson(this); +} + @JsonSerializable() class BackupServer { BackupServer({ diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 9475c47..fe5796d 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -15,28 +15,31 @@ UserData _$UserDataFromJson(Map 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?) ?.map((e) => e as String) .toList() - ..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ..autoDownloadOptions = (json['autoDownloadOptions'] as Map?)?.map( (k, e) => MapEntry(k, (e as List).map((e) => e as String).toList()), ) - ..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool? + ..storeMediaFilesInGallery = + json['storeMediaFilesInGallery'] as bool? ?? false ..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List?) ?.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?) ?.map((e) => e as String) .toList() @@ -45,26 +48,16 @@ UserData _$UserDataFromJson(Map 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) - ..twonlySafeEncryptionKey = - (json['twonlySafeEncryptionKey'] as List?) - ?.map((e) => (e as num).toInt()) - .toList() - ..twonlySafeBackupId = (json['twonlySafeBackupId'] as List?) - ?.map((e) => (e as num).toInt()) - .toList(); + ..twonlySafeBackup = json['twonlySafeBackup'] == null + ? null + : TwonlySafeBackup.fromJson( + json['twonlySafeBackup'] as Map); Map _$UserDataToJson(UserData instance) => { 'userId': instance.userId, @@ -74,31 +67,26 @@ Map _$UserDataToJson(UserData instance) => { '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 json) => + TwonlySafeBackup( + backupId: (json['backupId'] as List) + .map((e) => (e as num).toInt()) + .toList(), + encryptionKey: (json['encryptionKey'] as List) + .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 _$TwonlySafeBackupToJson(TwonlySafeBackup instance) => + { + '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 json) => BackupServer( serverUrl: json['serverUrl'] as String, retentionDays: (json['retentionDays'] as num).toInt(), diff --git a/lib/src/model/protobuf/api/websocket/error.pbenum.dart b/lib/src/model/protobuf/api/websocket/error.pbenum.dart index 8ba71da..a098d6f 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbenum.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbenum.dart @@ -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 values = [ 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); diff --git a/lib/src/model/protobuf/api/websocket/error.pbjson.dart b/lib/src/model/protobuf/api/websocket/error.pbjson.dart index 9d71562..47f586e 100644 --- a/lib/src/model/protobuf/api/websocket/error.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/error.pbjson.dart @@ -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'); diff --git a/lib/src/model/protobuf/backup/backup.pb.dart b/lib/src/model/protobuf/backup/backup.pb.dart new file mode 100644 index 0000000..5b93197 --- /dev/null +++ b/lib/src/model/protobuf/backup/backup.pb.dart @@ -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 createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TwonlySafeBackupContent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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 createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TwonlySafeBackupEncrypted getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(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'); diff --git a/lib/src/model/protobuf/backup/backup.pbenum.dart b/lib/src/model/protobuf/backup/backup.pbenum.dart new file mode 100644 index 0000000..184bdfe --- /dev/null +++ b/lib/src/model/protobuf/backup/backup.pbenum.dart @@ -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 + diff --git a/lib/src/model/protobuf/backup/backup.pbjson.dart b/lib/src/model/protobuf/backup/backup.pbjson.dart new file mode 100644 index 0000000..75b1e3d --- /dev/null +++ b/lib/src/model/protobuf/backup/backup.pbjson.dart @@ -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'); + diff --git a/lib/src/model/protobuf/backup/backup.pbserver.dart b/lib/src/model/protobuf/backup/backup.pbserver.dart new file mode 100644 index 0000000..1df01c1 --- /dev/null +++ b/lib/src/model/protobuf/backup/backup.pbserver.dart @@ -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'; + diff --git a/lib/src/model/protobuf/backup/backup.proto b/lib/src/model/protobuf/backup/backup.proto new file mode 100644 index 0000000..0a09bfd --- /dev/null +++ b/lib/src/model/protobuf/backup/backup.proto @@ -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; +} \ No newline at end of file diff --git a/lib/src/services/api/media_send.dart b/lib/src/services/api/media_send.dart index d967eee..568fab2 100644 --- a/lib/src/services/api/media_send.dart +++ b/lib/src/services/api/media_send.dart @@ -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}'); diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 08131c6..10b72bb 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -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, diff --git a/lib/src/services/backup.identitiy.service.dart b/lib/src/services/backup.identitiy.service.dart index 7d4b994..05db08a 100644 --- a/lib/src/services/backup.identitiy.service.dart +++ b/lib/src/services/backup.identitiy.service.dart @@ -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 createZipArchive(String zipFilePath, List 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; }); } diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 37b01e1..6522fa3 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -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> 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 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]}"; +} diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index c8c9bbd..5a99696 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -63,8 +63,8 @@ class SaveToGalleryButtonState extends State { 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"; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index fa017aa..5531f4b 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -176,9 +176,7 @@ class _CameraPreviewViewState extends State { 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(() {}); diff --git a/lib/src/views/chats/chat_list_components/backup_notice.card.dart b/lib/src/views/chats/chat_list_components/backup_notice.card.dart index 3b6d880..debd8c5 100644 --- a/lib/src/views/chats/chat_list_components/backup_notice.card.dart +++ b/lib/src/views/chats/chat_list_components/backup_notice.card.dart @@ -26,7 +26,7 @@ class _BackupNoticeCardState extends State { if (user != null && (user.nextTimeToShowBackupNotice == null || DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) { - if (!gIsDemoUser && (!user.identityBackupEnabled)) { + if (!gIsDemoUser && (user.twonlySafeBackup == null)) { showBackupNotice = true; } } diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index d69d65b..86fba28 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -320,7 +320,7 @@ class _MediaViewerViewState extends State { imageSaved = true; }); final user = await getUser(); - if (user != null && (user.storeMediaFilesInGallery ?? true)) { + if (user != null && (user.storeMediaFilesInGallery)) { if (videoPath != null) { await saveVideoToGallery(videoPath!); } else { diff --git a/lib/src/views/register.view.dart b/lib/src/views/register.view.dart index b2519be..aa68d8a 100644 --- a/lib/src/views/register.view.dart +++ b/lib/src/views/register.view.dart @@ -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 { 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, diff --git a/lib/src/views/settings/backup/backup.view.dart b/lib/src/views/settings/backup/backup.view.dart index 235f390..7fad0f3 100644 --- a/lib/src/views/settings/backup/backup.view.dart +++ b/lib/src/views/settings/backup/backup.view.dart @@ -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 createState() => _BackupViewState(); } +BackupServer defaultBackupServer = BackupServer( + serverUrl: "Default", + retentionDays: 180, + maxBackupBytes: 2097152, +); + class _BackupViewState extends State { - 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 { 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 { 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"), + ), + ) ], ), ), diff --git a/lib/src/views/settings/data_and_storage.view.dart b/lib/src/views/settings/data_and_storage.view.dart index 919c078..54b03dd 100644 --- a/lib/src/views/settings/data_and_storage.view.dart +++ b/lib/src/views/settings/data_and_storage.view.dart @@ -27,7 +27,7 @@ class _DataAndStorageViewState extends State { setState(() { autoDownloadOptions = user.autoDownloadOptions ?? defaultAutoDownloadOptions; - storeMediaFilesInGallery = user.storeMediaFilesInGallery ?? true; + storeMediaFilesInGallery = user.storeMediaFilesInGallery; }); } diff --git a/lib/src/views/settings/profile/modify_avatar.view.dart b/lib/src/views/settings/profile/modify_avatar.view.dart index 84e2d64..ca56b86 100644 --- a/lib/src/views/settings/profile/modify_avatar.view.dart +++ b/lib/src/views/settings/profile/modify_avatar.view.dart @@ -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(); diff --git a/lib/src/views/settings/profile/profile.view.dart b/lib/src/views/settings/profile/profile.view.dart index ba432ba..7aaa238 100644 --- a/lib/src/views/settings/profile/profile.view.dart +++ b/lib/src/views/settings/profile/profile.view.dart @@ -32,11 +32,7 @@ class _ProfileViewState extends State { 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; }); diff --git a/pubspec.yaml b/pubspec.yaml index c8dd962..c8f24ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: