add more ui and create hash value

This commit is contained in:
otsmr 2025-06-17 00:25:46 +02:00
parent 3f6b604f50
commit 4927d4adf4
16 changed files with 594 additions and 62 deletions

View file

@ -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.", "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.", "memoriesEmpty": "Sobald du Bilder oder Videos speicherst, landen sie hier in deinen Erinnerungen.",
"deleteImageTitle": "Bist du dir sicher?", "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"
} }

View file

@ -419,5 +419,9 @@
"memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.", "memoriesEmpty": "As soon as you save pictures or videos, they end up here in your memories.",
"deleteImageTitle": "Are you sure?", "deleteImageTitle": "Are you sure?",
"deleteImageBody": "The image will be irrevocably deleted.", "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"
} }

View file

@ -1579,6 +1579,30 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Backup'** /// **'Backup'**
String get settingsBackup; 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 class _AppLocalizationsDelegate

View file

@ -836,4 +836,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsBackup => 'Backup'; 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';
} }

View file

@ -830,4 +830,17 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsBackup => 'Backup'; 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';
} }

View file

@ -12,17 +12,21 @@ class UserData {
required this.isDemoUser, required this.isDemoUser,
}); });
String username; final int userId;
String displayName;
String? avatarSvg;
String? avatarJson;
int? avatarCounter;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool isDemoUser = false; bool isDemoUser = false;
// settings // -- USER PROFILE --
String username;
String displayName;
String? avatarSvg;
String? avatarJson;
int? avatarCounter;
// --- SETTINGS ---
int? defaultShowTime; int? defaultShowTime;
@JsonKey(defaultValue: "Preview") @JsonKey(defaultValue: "Preview")
String subscriptionPlan; String subscriptionPlan;
@ -44,13 +48,37 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated; DateTime? signalLastSignedPreKeyUpdated;
// --- BACKUP ---
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool identityBackupEnabled = false; bool identityBackupEnabled = false;
DateTime? identityBackupLastBackupTime; 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) => factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json); _$UserDataFromJson(json);
Map<String, dynamic> toJson() => _$UserDataToJson(this); 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);
}

View file

@ -49,15 +49,31 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..identityBackupLastBackupTime = ..identityBackupLastBackupTime =
json['identityBackupLastBackupTime'] == null json['identityBackupLastBackupTime'] == null
? 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>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId,
'isDemoUser': instance.isDemoUser,
'username': instance.username, 'username': instance.username,
'displayName': instance.displayName, 'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg, 'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson, 'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'isDemoUser': instance.isDemoUser,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'useHighQuality': instance.useHighQuality, 'useHighQuality': instance.useHighQuality,
@ -77,7 +93,12 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'identityBackupEnabled': instance.identityBackupEnabled, 'identityBackupEnabled': instance.identityBackupEnabled,
'identityBackupLastBackupTime': 'identityBackupLastBackupTime':
instance.identityBackupLastBackupTime?.toIso8601String(), instance.identityBackupLastBackupTime?.toIso8601String(),
'userId': instance.userId, 'identityBackupLastBackupSize': instance.identityBackupLastBackupSize,
'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer,
'twonlySafeEncryptionKey': instance.twonlySafeEncryptionKey,
'twonlySafeBackupId': instance.twonlySafeBackupId,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@ -85,3 +106,16 @@ const _$ThemeModeEnumMap = {
ThemeMode.light: 'light', ThemeMode.light: 'light',
ThemeMode.dark: 'dark', 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,
};

View file

@ -1 +1,57 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:hashlib/hashlib.dart';
import 'package:twonly/src/utils/storage.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));
}

View file

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:restart_app/restart_app.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/services/api/media_received.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/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/flame.dart';
import 'package:twonly/src/views/components/initialsavatar.dart'; import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart'; import 'package:twonly/src/views/components/message_send_state_icon.dart';
@ -179,48 +179,19 @@ class _ChatListViewState extends State<ChatListView> {
child: ListView.builder( child: ListView.builder(
itemCount: _pinnedContacts.length + itemCount: _pinnedContacts.length +
(_pinnedContacts.isNotEmpty ? 1 : 0) + (_pinnedContacts.isNotEmpty ? 1 : 0) +
_contacts.length, (gIsDemoUser ? 1 : 0) +
itemExtentBuilder: (index, dimensions) { _contacts.length +
int adjustedIndex = index - _pinnedContacts.length; 1,
if (_pinnedContacts.isNotEmpty && adjustedIndex == 0) {
return 16;
}
return 72;
},
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (gIsDemoUser && index == 0) { if (index == 0) {
return Container( return BackupNoticeCard();
color: isDarkMode(context) }
? Colors.white index -= 1;
: Colors.black, if (gIsDemoUser) {
child: Row( if (index == 0) {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, return DemoUserCard();
crossAxisAlignment: CrossAxisAlignment.center, }
children: [ index -= 1;
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"),
)
],
),
);
} }
// Check if the index is for the pinned users // Check if the index is for the pinned users
if (index < _pinnedContacts.length) { if (index < _pinnedContacts.length) {

View file

@ -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),
),
],
),
],
),
),
);
}
}

View 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"),
)
],
),
);
}
}

View file

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; 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/misc.dart';
import 'package:twonly/src/utils/storage.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 { class BackupView extends StatefulWidget {
const BackupView({super.key}); const BackupView({super.key});
@ -47,10 +48,14 @@ class _BackupViewState extends State<BackupView> {
lastBackup: _twonlyIdLastBackup, lastBackup: _twonlyIdLastBackup,
autoBackupEnabled: _twonlyIdBackupEnabled, autoBackupEnabled: _twonlyIdBackupEnabled,
onTap: () async { onTap: () async {
await Navigator.push(context, if (_twonlyIdBackupEnabled) {
MaterialPageRoute(builder: (context) { await disableTwonlySafe();
return TwonlyIdentityBackupView(); } else {
})); await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return TwonlyIdentityBackupView();
}));
}
initAsync(); initAsync();
}, },
), ),
@ -71,6 +76,7 @@ class _BackupViewState extends State<BackupView> {
class BackupOption extends StatelessWidget { class BackupOption extends StatelessWidget {
final String title; final String title;
final String description; final String description;
final Widget? child;
final bool autoBackupEnabled; final bool autoBackupEnabled;
final DateTime? lastBackup; final DateTime? lastBackup;
final Function() onTap; final Function() onTap;
@ -82,6 +88,7 @@ class BackupOption extends StatelessWidget {
required this.autoBackupEnabled, required this.autoBackupEnabled,
required this.lastBackup, required this.lastBackup,
required this.onTap, required this.onTap,
this.child,
}); });
String formatDateTime(DateTime? dateTime) { String formatDateTime(DateTime? dateTime) {
@ -118,6 +125,7 @@ class BackupOption extends StatelessWidget {
SizedBox(height: 8.0), SizedBox(height: 8.0),
Text(description), Text(description),
SizedBox(height: 8.0), SizedBox(height: 8.0),
(child != null) ? child! : Container(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View file

@ -1,7 +1,10 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/services/backup.identitiy.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.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_server.view.dart';
class TwonlyIdentityBackupView extends StatefulWidget { class TwonlyIdentityBackupView extends StatefulWidget {
const TwonlyIdentityBackupView({super.key}); const TwonlyIdentityBackupView({super.key});
@ -13,9 +16,25 @@ class TwonlyIdentityBackupView extends StatefulWidget {
class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> { class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
bool obscureText = true; bool obscureText = true;
bool isLoading = false;
final TextEditingController passwordCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -25,7 +44,7 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
IconButton( IconButton(
onPressed: () { onPressed: () {
showAlertDialog(context, "twonly Safe", 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), icon: FaIcon(FontAwesomeIcons.circleInfo),
iconSize: 18, iconSize: 18,
@ -118,6 +137,35 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
: Colors.transparent), : 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"),
))
], ],
), ),
), ),

View 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"),
),
)
],
),
),
);
}
}

View file

@ -787,6 +787,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" 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: html:
dependency: transitive dependency: transitive
description: description:

View file

@ -66,6 +66,7 @@ dependencies:
photo_view: ^0.15.0 photo_view: ^0.15.0
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
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: