Improve backup flow: add user controlled path dialog, better default path, and update text casing
This commit is contained in:
@@ -520,17 +520,23 @@
|
||||
"type": "Type",
|
||||
"period": "Period",
|
||||
"digits": "Digits",
|
||||
"localBackupSettingsTitle": "Local Backup Settings",
|
||||
"enableAutomaticBackups": "Enable Automatic Backups",
|
||||
"backupLocation": "Backup Location",
|
||||
"backupLocationDescription": "Select a folder to save backups. Backups run automatically when entries are added, deleted, or edited.",
|
||||
"currentLocation": "Current Location:",
|
||||
"changeLocation": "Change Backup Location",
|
||||
"securityNotice": "Security Notice",
|
||||
"localBackupSettingsTitle": "Local backup",
|
||||
"localBackupSidebarTitle": "Local backup",
|
||||
"enableAutomaticBackups": "Enable automatic backups",
|
||||
"backupLocation": "Backup location",
|
||||
"backupLocationDescription": "This will automatically backup your data to an on-device location. Backups are updated whenever entries are added, edited or deleted",
|
||||
"currentLocation": "Current backup location:",
|
||||
"changeLocation": "Change backup location",
|
||||
"securityNotice": "Security notice",
|
||||
"backupSecurityNotice": "This encrypted backup holds your 2FA keys. If lost, you may not be able to recover your accounts. Keep it safe!",
|
||||
"locationUpdatedAndBackupCreated": "Location updated and initial backup created!",
|
||||
"initialBackupCreated": "Initial backup created!",
|
||||
"pleaseChooseBackupLocation": "Please choose a backup location.",
|
||||
"passwordTooShort": "Password must be at least 8 characters long.",
|
||||
"noDefaultBackupFolder": "Could not create default backup folder."
|
||||
"noDefaultBackupFolder": "Could not create default backup folder.",
|
||||
"backupLocationChoiceDescription": "Where do you want to save your backups?",
|
||||
"defaultLocation": "Default location",
|
||||
"chooseBackupLocation": "Choose a backup location",
|
||||
"loadDefaultLocation": "Loading default location...",
|
||||
"couldNotDetermineLocation":"Could not determine location..."
|
||||
}
|
||||
@@ -92,8 +92,8 @@ class DataSectionWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Local Backup",
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.localBackupSidebarTitle,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
|
||||
@@ -4,9 +4,12 @@ import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/local_backup_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -38,96 +41,207 @@ class _LocalBackupSettingsPageState extends State<LocalBackupSettingsPage> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<String> _getDefaultBackupPath() async {
|
||||
Directory? dir;
|
||||
if (Platform.isAndroid) {
|
||||
dir = await getExternalStorageDirectory();
|
||||
//so default path would be /storage/emulated/0/Android/data/io.ente.auth/files/Downloads
|
||||
return '${dir!.path}/Downloads/EnteAuthBackups';
|
||||
} else {
|
||||
// Fallback for iOS and other platforms
|
||||
dir = await getDownloadsDirectory();
|
||||
return '${dir!.path}/EnteAuthBackups';
|
||||
Future<String?> _showCustomPasswordDialog() async {
|
||||
final l10n = context.l10n;
|
||||
final textController = TextEditingController();
|
||||
// state variable to track password visibility
|
||||
bool isPasswordHidden = true;
|
||||
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(l10n.setPasswordTitle),
|
||||
content: TextField(
|
||||
controller: textController,
|
||||
autofocus: true,
|
||||
obscureText: isPasswordHidden,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.enterPassword,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordHidden ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isPasswordHidden = !isPasswordHidden;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (text) => setState(() {}),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(null),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: textController.text.isNotEmpty
|
||||
? () => Navigator.of(context).pop(textController.text)
|
||||
: null,
|
||||
child: Text(l10n.saveAction),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _showLocationChoiceDialog() async {
|
||||
final l10n = context.l10n;
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
|
||||
final result = await showDialogWidget(
|
||||
title: l10n.chooseBackupLocation,
|
||||
context: context,
|
||||
body: l10n.backupLocationChoiceDescription,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: l10n.chooseBackupLocation,
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.first,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: l10n.defaultLocation,
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: l10n.cancel,
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.cancel,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// if user cancels or dismisses the dialog
|
||||
if (result?.action == null || result?.action == ButtonAction.cancel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result!.action == ButtonAction.first) {
|
||||
return await _pickAndSaveBackupLocation(successMessage: l10n.initialBackupCreated);
|
||||
} else if (result.action == ButtonAction.second) {
|
||||
// if user selects "Default location"
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
try {
|
||||
final String path = await _getDefaultBackupPath();
|
||||
await Directory(path).create(recursive: true);
|
||||
await prefs.setString('autoBackupPath', path);
|
||||
setState(() {
|
||||
_backupPath = path;
|
||||
});
|
||||
await LocalBackupService.instance.triggerAutomaticBackup();
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.initialBackupCreated)),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.noDefaultBackupFolder)),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<String> _getDefaultBackupPath() async {
|
||||
if (Platform.isAndroid) {
|
||||
Directory? externalDir = await getExternalStorageDirectory();
|
||||
if (externalDir != null) {
|
||||
String storagePath = externalDir.path.split('/Android')[0];
|
||||
return '$storagePath/Download/EnteAuthBackups';
|
||||
}
|
||||
}
|
||||
|
||||
Directory? dir = await getDownloadsDirectory();
|
||||
dir ??= await getApplicationDocumentsDirectory();
|
||||
return '${dir.path}/EnteAuthBackups';
|
||||
}
|
||||
}
|
||||
|
||||
// opens directory picker
|
||||
Future<void> _pickAndSaveBackupLocation() async {
|
||||
//to fetch the current backup path
|
||||
Future<bool> _pickAndSaveBackupLocation({String? successMessage}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final l10n = context.l10n;
|
||||
|
||||
String? directoryPath = await FilePicker.platform.getDirectoryPath();
|
||||
|
||||
if (directoryPath != null) {
|
||||
|
||||
await prefs.setString('autoBackupPath', directoryPath);
|
||||
|
||||
// we only set the state and create the backup if a path was chosen
|
||||
setState(() {
|
||||
_backupPath = directoryPath;
|
||||
});
|
||||
|
||||
await LocalBackupService.instance.triggerAutomaticBackup();
|
||||
final l10n = context.l10n;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.locationUpdatedAndBackupCreated),
|
||||
content: Text(successMessage ?? l10n.locationUpdatedAndBackupCreated),
|
||||
),
|
||||
);
|
||||
return true; // Report success
|
||||
}
|
||||
return false; // User cancelled the file picker
|
||||
}
|
||||
|
||||
Future<void> _showSetPasswordDialog() async {
|
||||
final l10n = context.l10n;
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: l10n.setPasswordTitle,
|
||||
submitButtonLabel: l10n.saveAction,
|
||||
hintText: l10n.enterPassword,
|
||||
isPasswordInput: true,
|
||||
onSubmit: (String password) async {
|
||||
if (password.length < 8) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.passwordTooShort),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'autoBackupPassword', value: password);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('isAutoBackupEnabled', true);
|
||||
|
||||
String? finalBackupPath = _backupPath;
|
||||
|
||||
if (finalBackupPath == null) {
|
||||
try {
|
||||
finalBackupPath = await _getDefaultBackupPath();
|
||||
await Directory(finalBackupPath).create(recursive: true);
|
||||
await prefs.setString('autoBackupPath', finalBackupPath);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.noDefaultBackupFolder),
|
||||
),
|
||||
);
|
||||
|
||||
await prefs.setBool('isAutoBackupEnabled', false);
|
||||
Future<void> _showSetPasswordDialog() async {
|
||||
final String? password = await _showCustomPasswordDialog();
|
||||
if (password == null) {
|
||||
setState(() {
|
||||
_isBackupEnabled = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.passwordTooShort),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_isBackupEnabled = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.write(key: 'autoBackupPassword', value: password);
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
final bool setupCompleted = await _showLocationChoiceDialog();
|
||||
if (!mounted) return;
|
||||
|
||||
if (setupCompleted) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('isAutoBackupEnabled', true);
|
||||
setState(() {
|
||||
_isBackupEnabled = true;
|
||||
});
|
||||
await LocalBackupService.instance.triggerAutomaticBackup();
|
||||
} else {
|
||||
setState(() {
|
||||
_isBackupEnabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isBackupEnabled = true;
|
||||
_backupPath = finalBackupPath;
|
||||
});
|
||||
|
||||
await LocalBackupService.instance.triggerAutomaticBackup();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.initialBackupCreated)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
@@ -137,155 +251,171 @@ ScaffoldMessenger.of(context).showSnackBar(
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
l10n.enableAutomaticBackups,
|
||||
style: getEnteTextTheme(context).largeBold,
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: _isBackupEnabled,
|
||||
activeColor: Theme.of(context).colorScheme.enteTheme.colorScheme.primary400,
|
||||
activeTrackColor: Theme.of(context).colorScheme.enteTheme.colorScheme.primary300,
|
||||
inactiveTrackColor: Theme.of(context).colorScheme.enteTheme.colorScheme.fillMuted,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (value == true) {
|
||||
//when toggle is ON, show password dialog
|
||||
await _showSetPasswordDialog();
|
||||
} else {
|
||||
await prefs.setBool('isAutoBackupEnabled', false);
|
||||
setState(() {
|
||||
_isBackupEnabled = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Opacity(
|
||||
opacity: _isBackupEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isBackupEnabled,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.backupLocation,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.enableAutomaticBackups,
|
||||
style: getEnteTextTheme(context).largeBold,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.backupLocationDescription,
|
||||
style: getEnteTextTheme(context).small,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.currentLocation,
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (_backupPath != null)
|
||||
Text(
|
||||
_backupPath!,
|
||||
style: getEnteTextTheme(context).small,
|
||||
)
|
||||
else
|
||||
FutureBuilder<String>(
|
||||
future: _getDefaultBackupPath(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Text(
|
||||
"Loading default location...",
|
||||
style: getEnteTextTheme(context).small.copyWith(color: Colors.grey),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
"Could not determine location.",
|
||||
style: getEnteTextTheme(context).small.copyWith(color: Colors.red),
|
||||
);
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: _isBackupEnabled,
|
||||
activeColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.enteTheme
|
||||
.colorScheme
|
||||
.primary400,
|
||||
activeTrackColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.enteTheme
|
||||
.colorScheme
|
||||
.primary300,
|
||||
inactiveTrackColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.enteTheme
|
||||
.colorScheme
|
||||
.fillMuted,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
} else {
|
||||
return Text(
|
||||
snapshot.data ?? '',
|
||||
style: getEnteTextTheme(context).small.copyWith(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _pickAndSaveBackupLocation,
|
||||
child: Text(l10n.changeLocation),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withAlpha(77),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
if (value == true) {
|
||||
//when toggle is ON, show password dialog
|
||||
await _showSetPasswordDialog();
|
||||
} else {
|
||||
await prefs.setBool('isAutoBackupEnabled', false);
|
||||
setState(() {
|
||||
_isBackupEnabled = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 50.0, top: 10.0),
|
||||
child: Text(
|
||||
l10n.backupLocationDescription,
|
||||
style: getEnteTextTheme(context).small,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Opacity(
|
||||
opacity: _isBackupEnabled ? 1.0 : 0.4,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isBackupEnabled,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.security_outlined,
|
||||
color: Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.securityNotice,
|
||||
style: getEnteTextTheme(context)
|
||||
.smallBold
|
||||
.copyWith(
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.backupSecurityNotice,
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
l10n.currentLocation,
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (_backupPath != null)
|
||||
Text(
|
||||
_backupPath!,
|
||||
style: getEnteTextTheme(context).small,
|
||||
)
|
||||
else
|
||||
FutureBuilder<String>(
|
||||
future: _getDefaultBackupPath(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.waiting) {
|
||||
return Text(
|
||||
l10n.loadDefaultLocation,
|
||||
style: getEnteTextTheme(context)
|
||||
.small
|
||||
.copyWith(color: Colors.grey),
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
l10n.couldNotDetermineLocation,
|
||||
style: getEnteTextTheme(context)
|
||||
.small
|
||||
.copyWith(color: Colors.red),
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
snapshot.data ?? '',
|
||||
style: getEnteTextTheme(context)
|
||||
.small
|
||||
.copyWith(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _pickAndSaveBackupLocation(),
|
||||
child: Text(l10n.changeLocation),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withAlpha(77),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.security_outlined,
|
||||
color: Colors.orange,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.securityNotice,
|
||||
style: getEnteTextTheme(context)
|
||||
.smallBold
|
||||
.copyWith(
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.backupSecurityNotice,
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -414,6 +414,7 @@ Future<dynamic> showTextInputDialog(
|
||||
bool alwaysShowSuccessState = false,
|
||||
bool isPasswordInput = false,
|
||||
bool useRootNavigator = false,
|
||||
VoidCallback? onCancel,
|
||||
}) {
|
||||
return showDialog(
|
||||
barrierColor: backdropFaintDark,
|
||||
|
||||
Reference in New Issue
Block a user