mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
add more ui and create hash value
This commit is contained in:
parent
3f6b604f50
commit
4927d4adf4
16 changed files with 594 additions and 62 deletions
|
|
@ -259,5 +259,9 @@
|
|||
"tutorialChatMessagesReopenMessageDesc": "Wenn dein Freund dir ein Bild oder Video mit unendlicher Anzeigezeit gesendet hat, kannst du es bis zum Neustart der App jederzeit erneut öffnen. Um dies zu tun, musst du einfach doppelt auf die Nachricht klicken. Dein Freund erhält dann eine Benachrichtigung, dass du das Bild erneut angesehen hast.",
|
||||
"memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.",
|
||||
"deleteImageTitle": "Bist du dir sicher?",
|
||||
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht."
|
||||
"deleteImageBody": "Das Bild wird unwiderruflich gelöscht.",
|
||||
"backupNoticeTitle": "Kein Backup konfiguriert",
|
||||
"backupNoticeDesc": "Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.",
|
||||
"backupNoticeLater": "Später erinnern",
|
||||
"backupNoticeOpenBackup": "Backup erstellen"
|
||||
}
|
||||
|
|
@ -419,5 +419,9 @@
|
|||
"memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.",
|
||||
"deleteImageTitle": "Are you sure?",
|
||||
"deleteImageBody": "The image will be irrevocably deleted.",
|
||||
"settingsBackup": "Backup"
|
||||
"settingsBackup": "Backup",
|
||||
"backupNoticeTitle": "No backup configured",
|
||||
"backupNoticeDesc": "If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.",
|
||||
"backupNoticeLater": "Remind later",
|
||||
"backupNoticeOpenBackup": "Create backup"
|
||||
}
|
||||
|
|
@ -1579,6 +1579,30 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Backup'**
|
||||
String get settingsBackup;
|
||||
|
||||
/// No description provided for @backupNoticeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No backup configured'**
|
||||
String get backupNoticeTitle;
|
||||
|
||||
/// No description provided for @backupNoticeDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.'**
|
||||
String get backupNoticeDesc;
|
||||
|
||||
/// No description provided for @backupNoticeLater.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remind later'**
|
||||
String get backupNoticeLater;
|
||||
|
||||
/// No description provided for @backupNoticeOpenBackup.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create backup'**
|
||||
String get backupNoticeOpenBackup;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -836,4 +836,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsBackup => 'Backup';
|
||||
|
||||
@override
|
||||
String get backupNoticeTitle => 'Kein Backup konfiguriert';
|
||||
|
||||
@override
|
||||
String get backupNoticeDesc =>
|
||||
'Wenn du dein Gerät wechselst oder verlierst, kann ohne Backup niemand dein Account wiederherstellen. Sichere deshalb deine Daten.';
|
||||
|
||||
@override
|
||||
String get backupNoticeLater => 'Später erinnern';
|
||||
|
||||
@override
|
||||
String get backupNoticeOpenBackup => 'Backup erstellen';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -830,4 +830,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get settingsBackup => 'Backup';
|
||||
|
||||
@override
|
||||
String get backupNoticeTitle => 'No backup configured';
|
||||
|
||||
@override
|
||||
String get backupNoticeDesc =>
|
||||
'If you change or lose your device, no one can restore your account without a backup. Therefore, back up your data.';
|
||||
|
||||
@override
|
||||
String get backupNoticeLater => 'Remind later';
|
||||
|
||||
@override
|
||||
String get backupNoticeOpenBackup => 'Create backup';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,21 @@ class UserData {
|
|||
required this.isDemoUser,
|
||||
});
|
||||
|
||||
String username;
|
||||
String displayName;
|
||||
|
||||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
int? avatarCounter;
|
||||
final int userId;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool isDemoUser = false;
|
||||
|
||||
// settings
|
||||
// -- USER PROFILE --
|
||||
|
||||
String username;
|
||||
String displayName;
|
||||
String? avatarSvg;
|
||||
String? avatarJson;
|
||||
int? avatarCounter;
|
||||
|
||||
// --- SETTINGS ---
|
||||
|
||||
int? defaultShowTime;
|
||||
@JsonKey(defaultValue: "Preview")
|
||||
String subscriptionPlan;
|
||||
|
|
@ -44,13 +48,37 @@ class UserData {
|
|||
|
||||
DateTime? signalLastSignedPreKeyUpdated;
|
||||
|
||||
// --- BACKUP ---
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool identityBackupEnabled = false;
|
||||
DateTime? identityBackupLastBackupTime;
|
||||
|
||||
final int userId;
|
||||
@JsonKey(defaultValue: 0)
|
||||
int identityBackupLastBackupSize = 0;
|
||||
DateTime? nextTimeToShowBackupNotice;
|
||||
BackupServer? backupServer;
|
||||
List<int>? twonlySafeEncryptionKey;
|
||||
List<int>? twonlySafeBackupId;
|
||||
|
||||
factory UserData.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserDataFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserDataToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BackupServer {
|
||||
BackupServer({
|
||||
required this.serverUrl,
|
||||
required this.retentionDays,
|
||||
required this.maxBackupBytes,
|
||||
});
|
||||
|
||||
String serverUrl;
|
||||
int retentionDays;
|
||||
int maxBackupBytes;
|
||||
|
||||
factory BackupServer.fromJson(Map<String, dynamic> json) =>
|
||||
_$BackupServerFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$BackupServerToJson(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,15 +49,31 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
|
|||
..identityBackupLastBackupTime =
|
||||
json['identityBackupLastBackupTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['identityBackupLastBackupTime'] as String);
|
||||
: DateTime.parse(json['identityBackupLastBackupTime'] as String)
|
||||
..identityBackupLastBackupSize =
|
||||
(json['identityBackupLastBackupSize'] as num?)?.toInt() ?? 0
|
||||
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
|
||||
? null
|
||||
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
|
||||
..backupServer = json['backupServer'] == null
|
||||
? null
|
||||
: BackupServer.fromJson(json['backupServer'] as Map<String, dynamic>)
|
||||
..twonlySafeEncryptionKey =
|
||||
(json['twonlySafeEncryptionKey'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..twonlySafeBackupId = (json['twonlySafeBackupId'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
'isDemoUser': instance.isDemoUser,
|
||||
'username': instance.username,
|
||||
'displayName': instance.displayName,
|
||||
'avatarSvg': instance.avatarSvg,
|
||||
'avatarJson': instance.avatarJson,
|
||||
'avatarCounter': instance.avatarCounter,
|
||||
'isDemoUser': instance.isDemoUser,
|
||||
'defaultShowTime': instance.defaultShowTime,
|
||||
'subscriptionPlan': instance.subscriptionPlan,
|
||||
'useHighQuality': instance.useHighQuality,
|
||||
|
|
@ -77,7 +93,12 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'identityBackupEnabled': instance.identityBackupEnabled,
|
||||
'identityBackupLastBackupTime':
|
||||
instance.identityBackupLastBackupTime?.toIso8601String(),
|
||||
'userId': instance.userId,
|
||||
'identityBackupLastBackupSize': instance.identityBackupLastBackupSize,
|
||||
'nextTimeToShowBackupNotice':
|
||||
instance.nextTimeToShowBackupNotice?.toIso8601String(),
|
||||
'backupServer': instance.backupServer,
|
||||
'twonlySafeEncryptionKey': instance.twonlySafeEncryptionKey,
|
||||
'twonlySafeBackupId': instance.twonlySafeBackupId,
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
|
@ -85,3 +106,16 @@ const _$ThemeModeEnumMap = {
|
|||
ThemeMode.light: 'light',
|
||||
ThemeMode.dark: 'dark',
|
||||
};
|
||||
|
||||
BackupServer _$BackupServerFromJson(Map<String, dynamic> json) => BackupServer(
|
||||
serverUrl: json['serverUrl'] as String,
|
||||
retentionDays: (json['retentionDays'] as num).toInt(),
|
||||
maxBackupBytes: (json['maxBackupBytes'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BackupServerToJson(BackupServer instance) =>
|
||||
<String, dynamic>{
|
||||
'serverUrl': instance.serverUrl,
|
||||
'retentionDays': instance.retentionDays,
|
||||
'maxBackupBytes': instance.maxBackupBytes,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1,57 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hashlib/hashlib.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future enableTwonlySafe(String password) async {
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
|
||||
final (backupId, encryptionKey) = await getMasterKey(password, user.username);
|
||||
|
||||
await updateUserdata((user) {
|
||||
user.identityBackupEnabled = true;
|
||||
user.twonlySafeBackupId = backupId.toList();
|
||||
user.twonlySafeEncryptionKey = encryptionKey.toList();
|
||||
return user;
|
||||
});
|
||||
startTwonlySafeBackup();
|
||||
}
|
||||
|
||||
Future disableTwonlySafe() async {
|
||||
await updateUserdata((user) {
|
||||
user.identityBackupEnabled = false;
|
||||
user.twonlySafeBackupId = null;
|
||||
user.twonlySafeEncryptionKey = null;
|
||||
user.identityBackupLastBackupTime = null;
|
||||
user.identityBackupLastBackupSize = 0;
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
Future startTwonlySafeBackup() async {
|
||||
print("startTwonlySafeBackup");
|
||||
}
|
||||
|
||||
Future<(Uint8List, Uint8List)> getMasterKey(
|
||||
String password,
|
||||
String username,
|
||||
) async {
|
||||
List<int> passwordBytes = utf8.encode(password);
|
||||
List<int> saltBytes = utf8.encode(username);
|
||||
|
||||
// Parameters for scrypt
|
||||
|
||||
// Create an instance of Scrypt
|
||||
final scrypt = Scrypt(
|
||||
cost: 65536,
|
||||
blockSize: 8,
|
||||
parallelism: 1,
|
||||
derivedKeyLength: 64,
|
||||
salt: saltBytes,
|
||||
);
|
||||
|
||||
// Derive the key
|
||||
final key = (await compute(scrypt.convert, passwordBytes)).bytes;
|
||||
return (key.sublist(0, 32), key.sublist(32, 64));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/api/media_received.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/backup_notice.card.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart';
|
||||
import 'package:twonly/src/views/chats/chat_list_components/demo_user.card.dart';
|
||||
import 'package:twonly/src/views/components/flame.dart';
|
||||
import 'package:twonly/src/views/components/initialsavatar.dart';
|
||||
import 'package:twonly/src/views/components/message_send_state_icon.dart';
|
||||
|
|
@ -179,48 +179,19 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
child: ListView.builder(
|
||||
itemCount: _pinnedContacts.length +
|
||||
(_pinnedContacts.isNotEmpty ? 1 : 0) +
|
||||
_contacts.length,
|
||||
itemExtentBuilder: (index, dimensions) {
|
||||
int adjustedIndex = index - _pinnedContacts.length;
|
||||
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
|
||||
return 16;
|
||||
}
|
||||
return 72;
|
||||
},
|
||||
(gIsDemoUser ? 1 : 0) +
|
||||
_contacts.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (gIsDemoUser && index == 0) {
|
||||
return Container(
|
||||
color: isDarkMode(context)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"This is a Demo-Preview.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: !isDarkMode(context)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
await deleteLocalUserData();
|
||||
Restart.restartApp(
|
||||
notificationTitle: 'Demo-Mode exited.',
|
||||
notificationBody:
|
||||
'Click here to open the app again',
|
||||
);
|
||||
},
|
||||
child: Text("Register"),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
if (index == 0) {
|
||||
return BackupNoticeCard();
|
||||
}
|
||||
index -= 1;
|
||||
if (gIsDemoUser) {
|
||||
if (index == 0) {
|
||||
return DemoUserCard();
|
||||
}
|
||||
index -= 1;
|
||||
}
|
||||
// Check if the index is for the pinned users
|
||||
if (index < _pinnedContacts.length) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
||||
class BackupNoticeCard extends StatefulWidget {
|
||||
const BackupNoticeCard({super.key});
|
||||
|
||||
@override
|
||||
State<BackupNoticeCard> createState() => _BackupNoticeCardState();
|
||||
}
|
||||
|
||||
class _BackupNoticeCardState extends State<BackupNoticeCard> {
|
||||
bool showBackupNotice = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initAsync();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
final user = await getUser();
|
||||
showBackupNotice = false;
|
||||
if (user != null &&
|
||||
(user.nextTimeToShowBackupNotice == null ||
|
||||
DateTime.now().isAfter(user.nextTimeToShowBackupNotice!))) {
|
||||
if (!gIsDemoUser && (!user.identityBackupEnabled)) {
|
||||
showBackupNotice = true;
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!showBackupNotice) return Container();
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: EdgeInsets.all(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.lang.backupNoticeTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Text(
|
||||
context.lang.backupNoticeDesc,
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await updateUserdata((user) {
|
||||
user.nextTimeToShowBackupNotice =
|
||||
DateTime.now().add(Duration(days: 7));
|
||||
return user;
|
||||
});
|
||||
initAsync();
|
||||
},
|
||||
child: Text(context.lang.backupNoticeLater),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BackupView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(context.lang.backupNoticeOpenBackup),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
39
lib/src/views/chats/chat_list_components/demo_user.card.dart
Normal file
39
lib/src/views/chats/chat_list_components/demo_user.card.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class DemoUserCard extends StatelessWidget {
|
||||
const DemoUserCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"This is a Demo-Preview.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: !isDarkMode(context) ? Colors.white : Colors.black,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
await deleteLocalUserData();
|
||||
Restart.restartApp(
|
||||
notificationTitle: 'Demo-Mode exited.',
|
||||
notificationBody: 'Click here to open the app again',
|
||||
);
|
||||
},
|
||||
child: Text("Register"),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:flutter/material.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/settings/backup/twonly_identity_backup.view.dart';
|
||||
import 'package:twonly/src/views/settings/backup/twonly_safe_backup.view.dart';
|
||||
|
||||
class BackupView extends StatefulWidget {
|
||||
const BackupView({super.key});
|
||||
|
|
@ -47,10 +48,14 @@ class _BackupViewState extends State<BackupView> {
|
|||
lastBackup: _twonlyIdLastBackup,
|
||||
autoBackupEnabled: _twonlyIdBackupEnabled,
|
||||
onTap: () async {
|
||||
if (_twonlyIdBackupEnabled) {
|
||||
await disableTwonlySafe();
|
||||
} else {
|
||||
await Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return TwonlyIdentityBackupView();
|
||||
}));
|
||||
}
|
||||
initAsync();
|
||||
},
|
||||
),
|
||||
|
|
@ -71,6 +76,7 @@ class _BackupViewState extends State<BackupView> {
|
|||
class BackupOption extends StatelessWidget {
|
||||
final String title;
|
||||
final String description;
|
||||
final Widget? child;
|
||||
final bool autoBackupEnabled;
|
||||
final DateTime? lastBackup;
|
||||
final Function() onTap;
|
||||
|
|
@ -82,6 +88,7 @@ class BackupOption extends StatelessWidget {
|
|||
required this.autoBackupEnabled,
|
||||
required this.lastBackup,
|
||||
required this.onTap,
|
||||
this.child,
|
||||
});
|
||||
|
||||
String formatDateTime(DateTime? dateTime) {
|
||||
|
|
@ -118,6 +125,7 @@ class BackupOption extends StatelessWidget {
|
|||
SizedBox(height: 8.0),
|
||||
Text(description),
|
||||
SizedBox(height: 8.0),
|
||||
(child != null) ? child! : Container(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/src/services/backup.identitiy.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/components/alert_dialog.dart';
|
||||
import 'package:twonly/src/views/settings/backup/twonly_safe_server.view.dart';
|
||||
|
||||
class TwonlyIdentityBackupView extends StatefulWidget {
|
||||
const TwonlyIdentityBackupView({super.key});
|
||||
|
|
@ -13,9 +16,25 @@ class TwonlyIdentityBackupView extends StatefulWidget {
|
|||
|
||||
class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
||||
bool obscureText = true;
|
||||
bool isLoading = false;
|
||||
final TextEditingController passwordCtrl = TextEditingController();
|
||||
final TextEditingController repeatedPasswordCtrl = TextEditingController();
|
||||
|
||||
Future onPressedEnableTwonlySafe() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
await enableTwonlySafe(passwordCtrl.text);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -25,7 +44,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
IconButton(
|
||||
onPressed: () {
|
||||
showAlertDialog(context, "twonly Safe",
|
||||
"Backup of your twonly-Identity. As twonly does not have any second factor like your phone number or email, this backup contains your twonly-Identity. If you lose your device, the only option to recover is with the twonly-ID Backup. This backup will be protected by a password chosen by you in the next step and anonymously uploaded to the twonly servers. Read more [here](https://twonly.eu/s/backup)");
|
||||
"twonly does not have any central user accounts. A key pair is created during installation, which consists of a public and a private key. The private key is only stored on your device to protect it from unauthorized access. The public key is uploaded to the server and linked to your chosen user name so that others can find you.\n\ntwonly Safe regularly creates an encrypted, anonymous backup of your private key together with your contacts and settings. Your username and chosen password are enough to restore this data on another device. ");
|
||||
},
|
||||
icon: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
iconSize: 18,
|
||||
|
|
@ -118,6 +137,35 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
|
|||
: Colors.transparent),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return TwonlySafeServerView();
|
||||
}));
|
||||
},
|
||||
child: Text("Experten Einstellungen"),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
onPressed: (!isLoading &&
|
||||
(passwordCtrl.text == repeatedPasswordCtrl.text &&
|
||||
passwordCtrl.text.length >= 10 ||
|
||||
kDebugMode))
|
||||
? onPressedEnableTwonlySafe
|
||||
: null,
|
||||
icon: isLoading
|
||||
? SizedBox(
|
||||
height: 12,
|
||||
width: 12,
|
||||
child: CircularProgressIndicator(strokeWidth: 1),
|
||||
)
|
||||
: Icon(Icons.lock_clock_rounded),
|
||||
label: Text("Automatisches Backup aktivieren"),
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
180
lib/src/views/settings/backup/twonly_safe_server.view.dart
Normal file
180
lib/src/views/settings/backup/twonly_safe_server.view.dart
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class TwonlySafeServerView extends StatefulWidget {
|
||||
const TwonlySafeServerView({super.key});
|
||||
|
||||
@override
|
||||
State<TwonlySafeServerView> createState() => _TwonlySafeServerViewState();
|
||||
}
|
||||
|
||||
class _TwonlySafeServerViewState extends State<TwonlySafeServerView> {
|
||||
final TextEditingController _urlController = TextEditingController();
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_urlController.text = "https://";
|
||||
super.initState();
|
||||
initAsync();
|
||||
}
|
||||
|
||||
Future initAsync() async {
|
||||
final user = await getUser();
|
||||
if (user?.backupServer != null) {
|
||||
var uri = Uri.parse(user!.backupServer!.serverUrl);
|
||||
// remove user auth data
|
||||
Uri serverUrl = Uri(
|
||||
scheme: uri.scheme,
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
path: uri.path,
|
||||
query: uri.query,
|
||||
);
|
||||
_urlController.text = serverUrl.toString();
|
||||
_usernameController.text = serverUrl.userInfo.split(":")[0].toString();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future checkAndUpdateBackupServer() async {
|
||||
String serverUrl = _urlController.text;
|
||||
if (!serverUrl.endsWith("/")) {
|
||||
serverUrl += "/";
|
||||
}
|
||||
|
||||
String username = _usernameController.text;
|
||||
String password = _passwordController.text;
|
||||
|
||||
if (username.isNotEmpty || password.isNotEmpty) {
|
||||
serverUrl = serverUrl.replaceAll("https://", "");
|
||||
serverUrl = "https://$username@$password$serverUrl";
|
||||
}
|
||||
|
||||
final uri = Uri.parse("${serverUrl}config");
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'User-Agent': 'twonly',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
if (response.statusCode == 200) {
|
||||
// If the server returns a 200 OK response, parse the JSON.
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
final backupServer = BackupServer(
|
||||
serverUrl: serverUrl,
|
||||
retentionDays: data["retentionDays"]!,
|
||||
maxBackupBytes: data["maxBackupBytes"]!);
|
||||
await updateUserdata((user) {
|
||||
user.backupServer = backupServer;
|
||||
return user;
|
||||
});
|
||||
if (mounted) Navigator.pop(context);
|
||||
} else {
|
||||
// If the server did not return a 200 OK response, throw an exception.
|
||||
throw Exception(
|
||||
'Got invalid status code ${response.statusCode} from server.');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error("$e");
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('$e'),
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('twonly Safe Server'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
Text(
|
||||
"Speichere dein twonly Safe-Backup bei twonly oder auf einem beliebigen Server deiner Wahl.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TextField(
|
||||
controller: _urlController,
|
||||
onChanged: (value) {
|
||||
if (value.length < 8) {
|
||||
value = "";
|
||||
}
|
||||
value = value.replaceAll("https://", "");
|
||||
value = value.replaceAll("http://", "");
|
||||
value = "https://$value";
|
||||
_urlController.text = value;
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Server URL',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username (optional)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password (optional)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
SizedBox(height: 20.0),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
onPressed: (_urlController.text.length > 8)
|
||||
? checkAndUpdateBackupServer
|
||||
: null,
|
||||
icon: FaIcon(FontAwesomeIcons.server),
|
||||
label: Text('Server verwenden'),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.0),
|
||||
Center(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await updateUserdata((user) {
|
||||
user.backupServer = null;
|
||||
return user;
|
||||
});
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
child: Text("Standardserver verwenden"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
pubspec.lock
16
pubspec.lock
|
|
@ -787,6 +787,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
hashlib:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hashlib
|
||||
sha256: c742f4250067e52686e2bbc73013794e748511473baa7f875289681436daa4ed
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
hashlib_codecs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hashlib_codecs
|
||||
sha256: "0e1a17c47792fd131a9bf49b811c394b22516287746ee14cd0b0c22a34136699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ dependencies:
|
|||
photo_view: ^0.15.0
|
||||
tutorial_coach_mark: ^1.3.0
|
||||
background_downloader: ^9.2.2
|
||||
hashlib: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in a new issue