backup creation and upload does work #121

This commit is contained in:
otsmr 2025-06-18 21:15:29 +02:00
parent fcc2c9fb9e
commit 9295de7b76
25 changed files with 705 additions and 153 deletions

View file

@ -2,6 +2,10 @@
# Set the source directory # 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/" SRC_DIR="../twonly-server/twonly/src/"
DST_DIR="$(pwd)/lib/src/model/protobuf/" DST_DIR="$(pwd)/lib/src/model/protobuf/"

View file

@ -4,4 +4,5 @@ class SecureStorageKeys {
static const String apiAuthToken = "api_auth_token"; static const String apiAuthToken = "api_auth_token";
static const String googleFcm = "google_fcm"; static const String googleFcm = "google_fcm";
static const String userData = "userData"; static const String userData = "userData";
static const String twonlySafeLastBackupHash = "twonly_safe_last_backup_hash";
} }

View file

@ -23,25 +23,39 @@ class UserData {
String displayName; String displayName;
String? avatarSvg; String? avatarSvg;
String? avatarJson; String? avatarJson;
int? avatarCounter;
@JsonKey(defaultValue: 0)
int avatarCounter = 0;
// --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: "Preview")
String subscriptionPlan;
DateTime? lastImageSend;
int? todaysImageCounter;
// --- SETTINGS --- // --- SETTINGS ---
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode themeMode = ThemeMode.system;
int? defaultShowTime; int? defaultShowTime;
@JsonKey(defaultValue: "Preview")
String subscriptionPlan; @JsonKey(defaultValue: true)
bool? useHighQuality; bool useHighQuality = true;
List<String>? preSelectedEmojies; List<String>? preSelectedEmojies;
ThemeMode? themeMode;
Map<String, List<String>>? autoDownloadOptions; Map<String, List<String>>? autoDownloadOptions;
bool? storeMediaFilesInGallery;
@JsonKey(defaultValue: false)
bool storeMediaFilesInGallery = false;
List<String>? lastUsedEditorEmojis; List<String>? lastUsedEditorEmojis;
String? lastPlanBallance; String? lastPlanBallance;
String? additionalUserInvites; String? additionalUserInvites;
DateTime? lastImageSend;
int? todaysImageCounter;
List<String>? tutorialDisplayed; List<String>? tutorialDisplayed;
int? myBestFriendContactId; int? myBestFriendContactId;
@ -50,22 +64,35 @@ class UserData {
// --- BACKUP --- // --- BACKUP ---
@JsonKey(defaultValue: false)
bool identityBackupEnabled = false;
DateTime? identityBackupLastBackupTime;
@JsonKey(defaultValue: 0)
int identityBackupLastBackupSize = 0;
DateTime? nextTimeToShowBackupNotice; DateTime? nextTimeToShowBackupNotice;
BackupServer? backupServer; BackupServer? backupServer;
List<int>? twonlySafeEncryptionKey; TwonlySafeBackup? twonlySafeBackup;
List<int>? twonlySafeBackupId;
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json); _$UserDataFromJson(json);
Map<String, dynamic> toJson() => _$UserDataToJson(this); 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() @JsonSerializable()
class BackupServer { class BackupServer {
BackupServer({ BackupServer({

View file

@ -15,28 +15,31 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
) )
..avatarSvg = json['avatarSvg'] as String? ..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] 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() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool? ..useHighQuality = json['useHighQuality'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?) ..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'])
..autoDownloadOptions = ..autoDownloadOptions =
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map( (json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
(k, e) => (k, e) =>
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()), 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>?) ..lastUsedEditorEmojis = (json['lastUsedEditorEmojis'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
..lastPlanBallance = json['lastPlanBallance'] as String? ..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] 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>?) ..tutorialDisplayed = (json['tutorialDisplayed'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
@ -45,26 +48,16 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null json['signalLastSignedPreKeyUpdated'] == null
? null ? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : 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 ..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null ? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String) : DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
..backupServer = json['backupServer'] == null ..backupServer = json['backupServer'] == null
? null ? null
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>) : BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
..twonlySafeEncryptionKey = ..twonlySafeBackup = json['twonlySafeBackup'] == null
(json['twonlySafeEncryptionKey'] as List<dynamic>?) ? null
?.map((e) => (e as num).toInt()) : TwonlySafeBackup.fromJson(
.toList() json['twonlySafeBackup'] as Map<String, dynamic>);
..twonlySafeBackupId = (json['twonlySafeBackupId'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList();
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId, 'userId': instance.userId,
@ -74,31 +67,26 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarSvg': instance.avatarSvg, 'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson, 'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'defaultShowTime': instance.defaultShowTime,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality, 'useHighQuality': instance.useHighQuality,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
'themeMode': _$ThemeModeEnumMap[instance.themeMode],
'autoDownloadOptions': instance.autoDownloadOptions, 'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'lastUsedEditorEmojis': instance.lastUsedEditorEmojis, 'lastUsedEditorEmojis': instance.lastUsedEditorEmojis,
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'tutorialDisplayed': instance.tutorialDisplayed, 'tutorialDisplayed': instance.tutorialDisplayed,
'myBestFriendContactId': instance.myBestFriendContactId, 'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated': 'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(), instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'identityBackupEnabled': instance.identityBackupEnabled,
'identityBackupLastBackupTime':
instance.identityBackupLastBackupTime?.toIso8601String(),
'identityBackupLastBackupSize': instance.identityBackupLastBackupSize,
'nextTimeToShowBackupNotice': 'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(), instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer, 'backupServer': instance.backupServer,
'twonlySafeEncryptionKey': instance.twonlySafeEncryptionKey, 'twonlySafeBackup': instance.twonlySafeBackup,
'twonlySafeBackupId': instance.twonlySafeBackupId,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@ -107,6 +95,39 @@ const _$ThemeModeEnumMap = {
ThemeMode.dark: 'dark', 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( BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
serverUrl: json['serverUrl'] as String, serverUrl: json['serverUrl'] as String,
retentionDays: (json['retentionDays'] as num).toInt(), retentionDays: (json['retentionDays'] as num).toInt(),

View file

@ -45,6 +45,7 @@ class ErrorCode extends $pb.ProtobufEnum {
static const ErrorCode PlanUpgradeNotYearly = ErrorCode._(1026, _omitEnumNames ? '' : 'PlanUpgradeNotYearly'); static const ErrorCode PlanUpgradeNotYearly = ErrorCode._(1026, _omitEnumNames ? '' : 'PlanUpgradeNotYearly');
static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey'); static const ErrorCode InvalidSignedPreKey = ErrorCode._(1027, _omitEnumNames ? '' : 'InvalidSignedPreKey');
static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound'); static const ErrorCode UserIdNotFound = ErrorCode._(1028, _omitEnumNames ? '' : 'UserIdNotFound');
static const ErrorCode UserIdAlreadyTaken = ErrorCode._(1029, _omitEnumNames ? '' : 'UserIdAlreadyTaken');
static const $core.List<ErrorCode> values = <ErrorCode> [ static const $core.List<ErrorCode> values = <ErrorCode> [
Unknown, Unknown,
@ -78,6 +79,7 @@ class ErrorCode extends $pb.ProtobufEnum {
PlanUpgradeNotYearly, PlanUpgradeNotYearly,
InvalidSignedPreKey, InvalidSignedPreKey,
UserIdNotFound, UserIdNotFound,
UserIdAlreadyTaken,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, ErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values);

View file

@ -48,6 +48,7 @@ const ErrorCode$json = {
{'1': 'PlanUpgradeNotYearly', '2': 1026}, {'1': 'PlanUpgradeNotYearly', '2': 1026},
{'1': 'InvalidSignedPreKey', '2': 1027}, {'1': 'InvalidSignedPreKey', '2': 1027},
{'1': 'UserIdNotFound', '2': 1028}, {'1': 'UserIdNotFound', '2': 1028},
{'1': 'UserIdAlreadyTaken', '2': 1029},
], ],
}; };
@ -66,5 +67,6 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'Cg5JbnZhbGlkUHJlS2V5cxD8BxITCg5Wb3VjaGVySW5WYWxpZBD9BxITCg5QbGFuTm90QWxsb3' 'Cg5JbnZhbGlkUHJlS2V5cxD8BxITCg5Wb3VjaGVySW5WYWxpZBD9BxITCg5QbGFuTm90QWxsb3'
'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q' 'dlZBD+BxIVChBQbGFuTGltaXRSZWFjaGVkEP8HEhQKD05vdEVub3VnaENyZWRpdBCACBISCg1Q'
'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW' 'bGFuRG93bmdyYWRlEIEIEhkKFFBsYW5VcGdyYWRlTm90WWVhcmx5EIIIEhgKE0ludmFsaWRTaW'
'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAg='); 'duZWRQcmVLZXkQgwgSEwoOVXNlcklkTm90Rm91bmQQhAgSFwoSVXNlcklkQWxyZWFkeVRha2Vu'
'EIUI');

View 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');

View 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

View 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');

View 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';

View 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;
}

View file

@ -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/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.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/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/notification.service.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -67,6 +68,9 @@ Future initFileDownloader() async {
if (update.task.taskId.contains("download_")) { if (update.task.taskId.contains("download_")) {
await handleDownloadStatusUpdate(update); await handleDownloadStatusUpdate(update);
} }
if (update.task.taskId.contains("backup")) {
await handleBackupStatusUpdate(update);
}
case TaskProgressUpdate(): case TaskProgressUpdate():
Log.info( Log.info(
'Progress update for ${update.task} with progress ${update.progress}'); 'Progress update for ${update.task} with progress ${update.progress}');

View file

@ -230,13 +230,12 @@ Future notifyContactsAboutProfileChange() async {
UserData? user = await getUser(); UserData? user = await getUser();
if (user == null) return; if (user == null) return;
if (user.avatarCounter == null) return;
if (user.avatarSvg == null) return; if (user.avatarSvg == null) return;
for (Contact contact in contacts) { for (Contact contact in contacts) {
if (contact.myAvatarCounter < user.avatarCounter!) { if (contact.myAvatarCounter < user.avatarCounter) {
twonlyDB.contactsDao.updateContact(contact.userId, twonlyDB.contactsDao.updateContact(contact.userId,
ContactsCompanion(myAvatarCounter: Value(user.avatarCounter!))); ContactsCompanion(myAvatarCounter: Value(user.avatarCounter)));
await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
null, null,
contact.userId, contact.userId,

View file

@ -1,34 +1,50 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; 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:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/hashlib.dart'; import 'package:hashlib/hashlib.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly_database.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/log.dart';
import 'package:twonly/src/utils/storage.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."); 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; final baseDir = (await getApplicationSupportDirectory()).path;
var originalDatabase = File(join(baseDir, "twonly_database.sqlite")); final backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
var backupDir = Directory(join(baseDir, "backup_twonly_safe/"));
if (backupDir.existsSync()) {
await backupDir.delete(recursive: true);
}
await backupDir.create(recursive: true); await backupDir.create(recursive: true);
var backupDatabaseFile = final backupDatabaseFile =
File(join(backupDir.path, "twonly_database.backup.sqlite")); File(join(backupDir.path, "twonly_database.backup.sqlite"));
// copy database // copy database
final originalDatabase = File(join(baseDir, "twonly_database.sqlite"));
await originalDatabase.copy(backupDatabaseFile.path); await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDatabase( final backupDB = TwonlyDatabase(
driftDatabase( driftDatabase(
name: "twonly_database.backup", name: "twonly_database.backup",
@ -48,30 +64,145 @@ Future performTwonlySafeBackup() async {
await storage.read(key: SecureStorageKeys.signalIdentity); await storage.read(key: SecureStorageKeys.signalIdentity);
secureStorageBackup[SecureStorageKeys.signalSignedPreKey] = secureStorageBackup[SecureStorageKeys.signalSignedPreKey] =
await storage.read(key: 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."); 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( await encryptedBackupBytesFile.writeAsBytes(encryptedBackupBytes);
twonlySafeBackupZip.path, [backupSecureStorage, backupDatabaseFile]);
// 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 { Future handleBackupStatusUpdate(TaskStatusUpdate update) async {
final encoder = ZipFileEncoder(); if (update.status == TaskStatus.failed ||
encoder.create(zipFilePath); update.status == TaskStatus.canceled) {
for (var file in filesToZip) { Log.error(
await encoder.addFile(file); "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 { Future enableTwonlySafe(String password) async {
@ -81,9 +212,10 @@ Future enableTwonlySafe(String password) async {
final (backupId, encryptionKey) = await getMasterKey(password, user.username); final (backupId, encryptionKey) = await getMasterKey(password, user.username);
await updateUserdata((user) { await updateUserdata((user) {
user.identityBackupEnabled = true; user.twonlySafeBackup = TwonlySafeBackup(
user.twonlySafeBackupId = backupId.toList(); encryptionKey: encryptionKey,
user.twonlySafeEncryptionKey = encryptionKey.toList(); backupId: backupId,
);
return user; return user;
}); });
startTwonlySafeBackup(); startTwonlySafeBackup();
@ -92,11 +224,7 @@ Future enableTwonlySafe(String password) async {
Future disableTwonlySafe() async { Future disableTwonlySafe() async {
await updateUserdata((user) { await updateUserdata((user) {
user.identityBackupEnabled = false; user.twonlySafeBackup = null;
user.twonlySafeBackupId = null;
user.twonlySafeEncryptionKey = null;
user.identityBackupLastBackupTime = null;
user.identityBackupLastBackupSize = 0;
return user; return user;
}); });
} }

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -384,3 +385,31 @@ List<List<dynamic>> chatMessages = [
], ],
["Curabitur blandit tempus porttitor.", DateTime.now()], ["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]}";
}

View file

@ -63,8 +63,8 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
memoryPath = join(memoryPath, token); memoryPath = join(memoryPath, token);
} }
final user = await getUser(); final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery ?? true)) {} if (user != null) return;
bool storeToGallery = user?.storeMediaFilesInGallery ?? true; bool storeToGallery = user!.storeMediaFilesInGallery;
if (widget.videoFilePath != null) { if (widget.videoFilePath != null) {
memoryPath += ".mp4"; memoryPath += ".mp4";

View file

@ -176,9 +176,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
void initAsync() async { void initAsync() async {
final user = await getUser(); final user = await getUser();
if (user == null) return; if (user == null) return;
if (user.useHighQuality != null) { useHighQuality = user.useHighQuality;
useHighQuality = user.useHighQuality!;
}
hasAudioPermission = await Permission.microphone.isGranted; hasAudioPermission = await Permission.microphone.isGranted;
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});

View file

@ -26,7 +26,7 @@ class _BackupNoticeCardState extends State<BackupNoticeCard> {
if (user != null && if (user != null &&
(user.nextTimeToShowBackupNotice == null || (user.nextTimeToShowBackupNotice == null ||
DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) { DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) {
if (!gIsDemoUser && (!user.identityBackupEnabled)) { if (!gIsDemoUser && (user.twonlySafeBackup == null)) {
showBackupNotice = true; showBackupNotice = true;
} }
} }

View file

@ -320,7 +320,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
imageSaved = true; imageSaved = true;
}); });
final user = await getUser(); final user = await getUser();
if (user != null && (user.storeMediaFilesInGallery ?? true)) { if (user != null && (user.storeMediaFilesInGallery)) {
if (videoPath != null) { if (videoPath != null) {
await saveVideoToGallery(videoPath!); await saveVideoToGallery(videoPath!);
} else { } else {

View file

@ -5,8 +5,10 @@ import 'package:twonly/globals.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:twonly/src/constants/secure_storage_keys.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/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/log.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/views/components/alert_dialog.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/utils/misc.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"); Log.info("Got user_id ${res.value} from server");
userId = res.value.userid.toInt(); userId = res.value.userid.toInt();
} else { } else {
if (res.error == ErrorCode.UserIdAlreadyTaken) {
Log.error("User ID already token. Tying again.");
await deleteLocalUserData();
return createNewUser();
}
if (mounted) { if (mounted) {
showAlertDialog( showAlertDialog(
context, context,

View file

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; 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/services/backup.identitiy.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart'; import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
Function() gUpdateBackupView = () {};
class BackupView extends StatefulWidget { class BackupView extends StatefulWidget {
const BackupView({super.key}); const BackupView({super.key});
@ -12,25 +16,54 @@ class BackupView extends StatefulWidget {
State<BackupView> createState() => _BackupViewState(); State<BackupView> createState() => _BackupViewState();
} }
BackupServer defaultBackupServer = BackupServer(
serverUrl: "Default",
retentionDays: 180,
maxBackupBytes: 2097152,
);
class _BackupViewState extends State<BackupView> { class _BackupViewState extends State<BackupView> {
bool _twonlyIdBackupEnabled = false; TwonlySafeBackup? twonlySafeBackup;
DateTime? _twonlyIdLastBackup; BackupServer backupServer = defaultBackupServer;
bool _dataBackupEnabled = false;
DateTime? _dataBackupLastBackup; int activePageIdx = 0;
final PageController pageController =
PageController(keepPage: true, initialPage: 0);
@override @override
void initState() { void initState() {
initAsync(); initAsync();
super.initState(); super.initState();
gUpdateBackupView = initAsync;
}
@override
void dispose() {
gUpdateBackupView = () {};
super.dispose();
} }
Future initAsync() async { Future initAsync() async {
final user = await getUser(); final user = await getUser();
if (user != null) { twonlySafeBackup = user?.twonlySafeBackup;
_twonlyIdBackupEnabled = user.identityBackupEnabled; backupServer = defaultBackupServer;
_twonlyIdLastBackup = user.identityBackupLastBackupTime; if (user?.backupServer != null) {
_dataBackupEnabled = false; backupServer = user!.backupServer!;
setState(() {}); }
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( appBar: AppBar(
title: Text(context.lang.settingsBackup), title: Text(context.lang.settingsBackup),
), ),
body: ListView( body: PageView(
controller: pageController,
onPageChanged: (index) {
setState(() {
activePageIdx = index;
});
},
children: [ children: [
BackupOption( BackupOption(
title: 'twonly Safe', title: 'twonly Safe',
description: description:
'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.', '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: twonlySafeBackup != null,
autoBackupEnabled: _twonlyIdBackupEnabled, 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 { onTap: () async {
if (_twonlyIdBackupEnabled) { if (twonlySafeBackup != null) {
bool disable = await showAlertDialog(context, "Are you sure?", bool disable = await showAlertDialog(context, "Are you sure?",
"Without an backup, you can not restore your user account."); "Without an backup, you can not restore your user account.");
if (disable) { if (disable) {
@ -68,12 +158,41 @@ class _BackupViewState extends State<BackupView> {
title: 'Daten-Backup (Coming Soon)', title: 'Daten-Backup (Coming Soon)',
description: 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.', '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, autoBackupEnabled: false,
onTap: () {}, onTap: null,
lastBackup: _dataBackupLastBackup,
), ),
], ],
), ),
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 String description;
final Widget? child; final Widget? child;
final bool autoBackupEnabled; final bool autoBackupEnabled;
final DateTime? lastBackup; final Function()? onTap;
final Function() onTap;
const BackupOption({ const BackupOption({
super.key, super.key,
required this.title, required this.title,
required this.description, required this.description,
required this.autoBackupEnabled, required this.autoBackupEnabled,
required this.lastBackup,
required this.onTap, required this.onTap,
this.child, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: (autoBackupEnabled) ? null : onTap, onTap: (autoBackupEnabled) ? null : onTap,
child: Card( child: Card(
margin: EdgeInsets.all(8.0), margin: EdgeInsets.all(16.0),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -131,21 +232,18 @@ class BackupOption extends StatelessWidget {
Text(description), Text(description),
SizedBox(height: 8.0), SizedBox(height: 8.0),
(child != null) ? child! : Container(), (child != null) ? child! : Container(),
Row( Expanded(child: Container()),
mainAxisAlignment: MainAxisAlignment.spaceBetween, Center(
children: [ child: (autoBackupEnabled)
Text('Last backup: ${formatDateTime(lastBackup)}'), ? OutlinedButton(
(autoBackupEnabled) onPressed: onTap,
? OutlinedButton( child: Text("Disable"),
onPressed: onTap, )
child: Text("Disable"), : FilledButton(
) onPressed: onTap,
: FilledButton( child: Text("Enable"),
onPressed: onTap, ),
child: Text("Enable"), )
)
],
),
], ],
), ),
), ),

View file

@ -27,7 +27,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
setState(() { setState(() {
autoDownloadOptions = autoDownloadOptions =
user.autoDownloadOptions ?? defaultAutoDownloadOptions; user.autoDownloadOptions ?? defaultAutoDownloadOptions;
storeMediaFilesInGallery = user.storeMediaFilesInGallery ?? true; storeMediaFilesInGallery = user.storeMediaFilesInGallery;
}); });
} }

View file

@ -14,11 +14,7 @@ class ModifyAvatar extends StatelessWidget {
await updateUserdata((user) { await updateUserdata((user) {
user.avatarJson = json; user.avatarJson = json;
user.avatarSvg = svg; user.avatarSvg = svg;
if (user.avatarCounter == null) { user.avatarCounter = user.avatarCounter + 1;
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
return user; return user;
}); });
await notifyContactsAboutProfileChange(); await notifyContactsAboutProfileChange();

View file

@ -32,11 +32,7 @@ class _ProfileViewState extends State<ProfileView> {
Future updateUserDisplayName(String displayName) async { Future updateUserDisplayName(String displayName) async {
await updateUserdata((user) { await updateUserdata((user) {
user.displayName = displayName; user.displayName = displayName;
if (user.avatarCounter == null) { user.avatarCounter = user.avatarCounter + 1;
user.avatarCounter = 1;
} else {
user.avatarCounter = user.avatarCounter! + 1;
}
return user; return user;
}); });

View file

@ -67,7 +67,6 @@ dependencies:
tutorial_coach_mark: ^1.3.0 tutorial_coach_mark: ^1.3.0
background_downloader: ^9.2.2 background_downloader: ^9.2.2
hashlib: ^2.0.0 hashlib: ^2.0.0
archive: ^4.0.7
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: