twonly-app/lib/src/views/settings/backup/setup_backup.view.dart
2026-04-11 00:09:14 +02:00

255 lines
8.2 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/backup/common.backup.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
class SetupBackupView extends StatefulWidget {
const SetupBackupView({
this.isPasswordChangeOnly = false,
this.callBack,
super.key,
});
// in case a callback is defined the callback
// is called instead of the Navigator.pop()
final VoidCallback? callBack;
final bool isPasswordChangeOnly;
@override
State<SetupBackupView> createState() => _SetupBackupViewState();
}
class _SetupBackupViewState extends State<SetupBackupView> {
bool obscureText = true;
bool isLoading = false;
final TextEditingController passwordCtrl = TextEditingController();
final TextEditingController repeatedPasswordCtrl = TextEditingController();
Future<void> onPressedEnableTwonlySafe() async {
setState(() {
isLoading = true;
});
if (!await isSecurePassword(passwordCtrl.text)) {
if (!mounted) return;
final ignore = await showAlertDialog(
context,
context.lang.backupInsecurePassword,
context.lang.backupInsecurePasswordDesc,
customCancel: context.lang.backupInsecurePasswordOk,
customOk: context.lang.backupInsecurePasswordCancel,
);
if (ignore) {
if (mounted) {
setState(() {
isLoading = false;
});
}
return;
}
}
setState(() {
isLoading = true;
});
await Future.delayed(const Duration(milliseconds: 100));
await enableTwonlySafe(passwordCtrl.text);
if (!mounted) return;
setState(() {
isLoading = false;
});
if (widget.callBack != null) {
widget.callBack!();
} else {
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: const Text('twonly Backup'),
actions: [
IconButton(
onPressed: () async {
await showAlertDialog(
context,
'twonly Backup',
context.lang.backupTwonlySafeLongDesc,
);
},
icon: const FaIcon(FontAwesomeIcons.circleInfo),
iconSize: 18,
),
],
),
body: Padding(
padding: const EdgeInsetsGeometry.symmetric(
vertical: 40,
horizontal: 40,
),
child: ListView(
children: [
Text(
context.lang.backupSelectStrongPassword,
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Stack(
children: [
TextField(
controller: passwordCtrl,
onChanged: (value) {
setState(() {});
},
style: const TextStyle(fontSize: 17),
obscureText: obscureText,
decoration: getInputDecoration(
context,
context.lang.password,
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: IconButton(
onPressed: () {
setState(() {
obscureText = !obscureText;
});
},
icon: FaIcon(
obscureText
? FontAwesomeIcons.eye
: FontAwesomeIcons.eyeSlash,
size: 16,
),
),
),
],
),
Padding(
padding: const EdgeInsetsGeometry.all(5),
child: Text(
context.lang.backupPasswordRequirement,
style: TextStyle(
fontSize: 13,
color:
(passwordCtrl.text.length < 8 &&
passwordCtrl.text.isNotEmpty)
? Colors.red
: Colors.transparent,
),
),
),
const SizedBox(height: 5),
TextField(
controller: repeatedPasswordCtrl,
onChanged: (value) {
setState(() {});
},
style: const TextStyle(fontSize: 17),
obscureText: true,
decoration: getInputDecoration(
context,
context.lang.passwordRepeated,
),
),
Padding(
padding: const EdgeInsetsGeometry.all(5),
child: Text(
context.lang.passwordRepeatedNotEqual,
style: TextStyle(
fontSize: 13,
color:
(passwordCtrl.text != repeatedPasswordCtrl.text &&
repeatedPasswordCtrl.text.isNotEmpty)
? Colors.red
: Colors.transparent,
),
),
),
const SizedBox(height: 10),
Center(
child: OutlinedButton(
onPressed: () => context.push(Routes.settingsBackupServer),
child: Text(context.lang.backupExpertSettings),
),
),
const SizedBox(height: 10),
Text(
context.lang.backupNoPasswordRecovery,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 10),
Center(
child: FilledButton.icon(
onPressed:
(!isLoading &&
(passwordCtrl.text == repeatedPasswordCtrl.text &&
passwordCtrl.text.length >= 8 ||
!kReleaseMode))
? onPressedEnableTwonlySafe
: null,
icon: isLoading
? const SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(strokeWidth: 1),
)
: const Icon(Icons.lock_clock_rounded),
label: Text(
widget.isPasswordChangeOnly
? context.lang.backupChangePassword
: context.lang.backupEnableBackup,
),
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
if (widget.callBack != null) {
widget.callBack!();
} else {
Navigator.pop(context);
}
},
child: Text(
context.lang.skipForNow,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 8, color: Colors.grey),
),
),
],
),
),
),
);
}
}
Future<bool> isSecurePassword(String password) async {
final badPasswordsStr = await rootBundle.loadString(
'assets/passwords/bad_passwords.txt',
);
final badPasswords = badPasswordsStr.split('\n');
if (badPasswords.contains(password)) {
return false;
}
// Check if the password meets all criteria
return RegExp('[A-Z]').hasMatch(password) &&
RegExp('[a-z]').hasMatch(password) &&
RegExp('[0-9]').hasMatch(password);
}