Compare commits

...

17 Commits

Author SHA1 Message Date
a5xwin
88d96b89fa Merge remote-tracking branch 'origin/main' into autobackup 2025-09-11 11:13:15 +05:30
a5xwin
0a1bcc863b fixed review changes 2025-09-11 11:12:19 +05:30
a5xwin
8e9a43564a Merge remote-tracking branch 'origin/main' into autobackup 2025-09-10 17:08:27 +05:30
a5xwin
fdbc248228 fixed review changes 2025-09-10 17:06:52 +05:30
a5xwin
fe732f2778 Merge remote-tracking branch 'origin/main' into autobackup 2025-08-24 22:58:36 +05:30
a5xwin
ca8a067966 added support to view backup path dynamically & improved ui flow 2025-08-24 22:57:35 +05:30
a5xwin
5e3a779925 Merge remote-tracking branch 'origin/main' into autobackup 2025-08-22 19:32:03 +05:30
a5xwin
d1b06abada Improve backup flow: add user controlled path dialog, better default path, and update text casing 2025-08-22 19:21:05 +05:30
a5xwin
9e70dc4312 Merge remote-tracking branch 'origin/main' into autobackup 2025-08-19 15:54:01 +05:30
a5xwin
541d71f65c added encryption feature to backups + refined backup logic 2025-08-19 15:07:23 +05:30
a5xwin
d8fc369a21 Merge remote-tracking branch 'origin/main' into autobackup 2025-08-17 00:15:50 +05:30
a5xwin
8efbebe9c4 Merge remote-tracking branch 'origin/main' into autobackup 2025-08-16 18:36:43 +05:30
a5xwin
a7300b7ac7 Restore local changes from stash 2025-08-15 16:07:51 +05:30
a5xwin
9224cea96f Fixed linting issues 2025-08-14 00:45:11 +05:30
a5xwin
9fbc618d69 Updated pubspec.lock regeneration 2025-08-14 00:02:04 +05:30
a5xwin
4614428f76 Resolved pubspec.lock conflict by taking main version 2025-08-13 23:36:49 +05:30
a5xwin
6fde4ee45f implemented an auto backup feature 2025-08-13 23:25:48 +05:30
13 changed files with 732 additions and 18 deletions

View File

@@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
android/app/build/
# Editors
.vscode/

View File

@@ -527,5 +527,24 @@
"errorInvalidQRCodeBody": "The scanned QR code is not a valid 2FA account.",
"errorNoQRCode": "No QR code found",
"errorGenericTitle": "An Error Occurred",
"errorGenericBody": "An unexpected error occurred while importing."
"errorGenericBody": "An unexpected error occurred while importing.",
"localBackupSettingsTitle": "Local backup",
"localBackupSidebarTitle": "Local backup",
"enableAutomaticBackups": "Enable automatic backups",
"backupDescription": "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:",
"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!",
"passwordTooShort": "Password must be at least 8 characters long.",
"noDefaultBackupFolder": "Could not create default backup folder.",
"backupLocationChoiceDescription": "Where do you want to save your backups?",
"chooseBackupLocation": "Choose a backup location",
"loadDefaultLocation": "Loading default location...",
"couldNotDetermineLocation":"Could not determine location...",
"saveAction":"Save",
"saveBackup":"Save backup",
"changeLocation": "Change location",
"changeCurrentLocation": "Change current location"
}

View File

@@ -0,0 +1,177 @@
import 'dart:convert';
import 'dart:io';
import 'package:ente_auth/models/export/ente.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:intl/intl.dart'; //for time based file naming
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
//we gonn change
class LocalBackupService {
final _logger = Logger('LocalBackupService');
static final LocalBackupService instance =
LocalBackupService._privateConstructor();
LocalBackupService._privateConstructor();
static const int _maxBackups = 2;
// to create an encrypted backup file if the toggle is on
Future<void> triggerAutomaticBackup() async {
try {
final prefs = await SharedPreferences.getInstance();
final isEnabled = prefs.getBool('isAutoBackupEnabled') ?? false;
if (!isEnabled) {
return;
}
final backupPath = prefs.getString('autoBackupPath');
if (backupPath == null) {
return;
}
const storage = FlutterSecureStorage();
final password = await storage.read(key: 'autoBackupPassword');
if (password == null || password.isEmpty) {
_logger.warning("Automatic backup skipped: password not set.");
return;
}
_logger.info("Change detected, triggering automatic encrypted backup...");
String rawContent = await CodeStore.instance.getCodesForExport();
List<String> lines = rawContent.split('\n');
List<String> cleanedLines = [];
for (String line in lines) {
if (line.trim().isEmpty) continue;
String cleanUrl;
if (line.startsWith('"') && line.endsWith('"')) {
cleanUrl = jsonDecode(line);
}
else {
cleanUrl = line;
}
cleanedLines.add(cleanUrl);
}
final plainTextContent = cleanedLines.join('\n');
if (plainTextContent.trim().isEmpty) {
return;
}
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password),
kekSalt,
);
final encResult = await CryptoUtil.encryptData(
utf8.encode(plainTextContent),
derivedKeyResult.key,
);
final encContent = CryptoUtil.bin2base64(encResult.encryptedData!);
final encNonce = CryptoUtil.bin2base64(encResult.header!);
final EnteAuthExport data = EnteAuthExport(
version: 1,
encryptedData: encContent,
encryptionNonce: encNonce,
kdfParams: KDFParams(
memLimit: derivedKeyResult.memLimit,
opsLimit: derivedKeyResult.opsLimit,
salt: CryptoUtil.bin2base64(kekSalt),
),
);
final encryptedJson = jsonEncode(data.toJson());
final now = DateTime.now();
final formatter = DateFormat('yyyy-MM-dd_HH-mm-ss');
final formattedDate = formatter.format(now);
final fileName = 'ente-auth-auto-backup-$formattedDate.json';
final filePath = '$backupPath/$fileName';
final backupFile = File(filePath);
await backupFile.writeAsString(encryptedJson);
await _manageOldBackups(backupPath);
_logger.info('Automatic encrypted backup successful! Saved to: $filePath');
} catch (e, s) {
_logger.severe('Silent error during automatic backup', e, s);
}
}
Future<void> _manageOldBackups(String backupPath) async {
try {
_logger.info("Checking for old backups to clean up...");
final directory = Directory(backupPath);
// fetch all filenames in the folder, filter out ente backup files
final files = directory.listSync()
.where((entity) =>
entity is File &&
entity.path.split('/').last.startsWith('ente-auth-auto-backup-'),)
.map((entity) => entity as File)
.toList();
// sort the fetched files in asc order (oldest first because the name is a timestamp)
files.sort((a, b) => a.path.compareTo(b.path));
// if we have more files than our limit, delete the oldest ones (current limit=_maxBackups)
while (files.length > _maxBackups) {
// remove the oldest file (at index 0) from the list
final fileToDelete = files.removeAt(0);
// and delete it from the device's storage..
await fileToDelete.delete();
_logger.info('Deleted old backup: ${fileToDelete.path}');
}
_logger.info('Backup count is now ${files.length}. Cleanup complete.');
} catch (e, s) {
_logger.severe('Error during old backup cleanup', e, s);
}
}
Future<void> deleteAllBackupsIn(String path) async {
try {
_logger.info("Deleting all backups in old location: $path");
final directory = Directory(path);
if (!await directory.exists()) {
_logger.warning("Old backup directory not found. Nothing to delete.");
return;
}
final files = directory.listSync()
.where((entity) =>
entity is File &&
entity.path.split('/').last.startsWith('ente-auth-auto-backup-'),)
.map((entity) => entity as File)
.toList();
if (files.isEmpty) {
_logger.info("No old backup files found to delete.");
return;
}
for (final file in files) {
await file.delete();
_logger.info('Deleted: ${file.path}');
}
_logger.info("Successfully cleaned up old backup location.");
} catch (e, s) {
_logger.severe('Error during full backup cleanup of old directory', e, s);
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/models/authenticator/entity_result.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/services/local_backup_service.dart';
import 'package:ente_auth/store/offline_authenticator_db.dart';
import 'package:ente_events/event_bus.dart';
import 'package:logging/logging.dart';
@@ -64,6 +65,27 @@ class CodeStore {
return true;
}
Future<void> updateCode(Code originalCode, Code updatedCode, {bool shouldSync = true}) async {
if (updatedCode.generatedID == null) return;
await _authenticatorService.updateEntry(
updatedCode.generatedID!,
updatedCode.toOTPAuthUrlFormat(),
shouldSync,
_authenticatorService.getAccountMode(),
);
Bus.instance.fire(CodesUpdatedEvent());
final bool isMajorChange = originalCode.issuer != updatedCode.issuer ||
originalCode.account != updatedCode.account ||
originalCode.secret != updatedCode.secret ||
originalCode.display.note != updatedCode.display.note;
if (isMajorChange) {
LocalBackupService.instance.triggerAutomaticBackup().ignore();
}
}
Future<List<Code>> getAllCodes({
AccountMode? accountMode,
bool sortCodes = true,
@@ -95,7 +117,6 @@ class CodeStore {
}
if (sortCodes) {
// sort codes by issuer,account
codes.sort((firstCode, secondCode) {
if (secondCode.isPinned && !firstCode.isPinned) return 1;
if (!secondCode.isPinned && firstCode.isPinned) return -1;
@@ -120,13 +141,17 @@ class CodeStore {
bool shouldSync = true,
AccountMode? accountMode,
List<Code>? existingAllCodes,
bool isFrequencyOrRecencyUpdate = false,
}) async {
final mode = accountMode ?? _authenticatorService.getAccountMode();
final allCodes = existingAllCodes ?? (await getAllCodes(accountMode: mode));
bool isExistingCode = false;
bool hasSameCode = false;
for (final existingCode in allCodes) {
if (existingCode.hasError) continue;
if (code.generatedID != null &&
existingCode.generatedID == code.generatedID) {
isExistingCode = true;
@@ -148,6 +173,9 @@ class CodeStore {
shouldSync,
mode,
);
if (!isFrequencyOrRecencyUpdate) {
LocalBackupService.instance.triggerAutomaticBackup().ignore();
}
} else {
result = AddResult.newCode;
code.generatedID = await _authenticatorService.addEntry(
@@ -155,6 +183,7 @@ class CodeStore {
shouldSync,
mode,
);
LocalBackupService.instance.triggerAutomaticBackup().ignore();
}
Bus.instance.fire(CodesUpdatedEvent());
return result;
@@ -164,6 +193,7 @@ class CodeStore {
final mode = accountMode ?? _authenticatorService.getAccountMode();
await _authenticatorService.deleteEntry(code.generatedID!, mode);
Bus.instance.fire(CodesUpdatedEvent());
LocalBackupService.instance.triggerAutomaticBackup().ignore();
}
bool _isOfflineImportRunning = false;
@@ -214,7 +244,6 @@ class CodeStore {
'importingCode: genID ${eachCode.generatedID} & isAlreadyPresent $alreadyPresent',
);
if (!alreadyPresent) {
// Avoid conflict with generatedID of online codes
eachCode.generatedID = null;
final AddResult result = await CodeStore.instance.addCode(
eachCode,
@@ -236,10 +265,21 @@ class CodeStore {
_isOfflineImportRunning = false;
}
}
Future<String> getCodesForExport() async {
final allCodes = await getAllCodes(sortCodes: false);
String data = "";
for (final code in allCodes) {
if (code.hasError) continue;
data += "${code.toOTPAuthUrlFormat()}\n";
}
return data;
}
}
enum AddResult {
newCode,
duplicate,
updateCode,
}
}

View File

@@ -478,7 +478,7 @@ class _CodeWidgetState extends State<CodeWidget> {
_getCurrentOTP(),
confirmationMessage: context.l10n.copiedToClipboard,
);
_udateCodeMetadata().ignore();
_updateCodeMetadata().ignore();
}
void _copyNextToClipboard() {
@@ -486,10 +486,10 @@ class _CodeWidgetState extends State<CodeWidget> {
_getNextTotp(),
confirmationMessage: context.l10n.copiedNextToClipboard,
);
_udateCodeMetadata().ignore();
_updateCodeMetadata().ignore();
}
Future<void> _udateCodeMetadata() async {
Future<void> _updateCodeMetadata() async {
if (widget.sortKey == null) return;
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
@@ -502,7 +502,7 @@ class _CodeWidgetState extends State<CodeWidget> {
lastUsedAt: DateTime.now().microsecondsSinceEpoch,
),
);
unawaited(CodeStore.instance.addCode(code));
unawaited(CodeStore.instance.addCode(code, isFrequencyOrRecencyUpdate: true));
}
}
});

View File

@@ -94,6 +94,7 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
_codeSortKey = PreferenceService.instance.codeSortKey();
_textController.addListener(_applyFilteringAndRefresh);
_loadCodes();
@@ -153,6 +154,7 @@ class _HomePageState extends State<HomePage> {
}
void _loadCodes() {
debugPrint("[HOME_DEBUG] _loadCodes triggered!");
CodeStore.instance.getAllCodes().then((codes) {
_allCodes = codes;
hasTrashedCodes = false;
@@ -817,4 +819,4 @@ class _HomePageState extends State<HomePage> {
],
);
}
}
}

View File

@@ -10,6 +10,7 @@ import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart';
import 'package:ente_auth/ui/settings/data/export_widget.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/ui/settings/data/local_backup_settings_page.dart'; //for local backup
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
@@ -29,6 +30,10 @@ class DataSectionWidget extends StatelessWidget {
);
}
Future<void> _handleLocalBackupClick(BuildContext context) async {
await routeToPage(context, const LocalBackupSettingsPage());
}
Column _getSectionOptions(BuildContext context) {
final l10n = context.l10n;
List<Widget> children = [];
@@ -86,10 +91,21 @@ class DataSectionWidget extends StatelessWidget {
);
},
),
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.localBackupSidebarTitle,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
await _handleLocalBackupClick(context);
},
),
sectionOptionSpacing,
]);
return Column(
children: children,
);
}
}
}

View File

@@ -153,4 +153,4 @@ Future<void> _pickEnteJsonFile(BuildContext context) async {
context.l10n.importFailureDescNew,
);
}
}
}

View File

@@ -0,0 +1,458 @@
import 'dart:io';
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/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.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';
class LocalBackupSettingsPage extends StatefulWidget {
const LocalBackupSettingsPage({super.key});
@override
State<LocalBackupSettingsPage> createState() =>
_LocalBackupSettingsPageState();
}
class _LocalBackupSettingsPageState extends State<LocalBackupSettingsPage> {
bool _isBackupEnabled = false;
String? _backupPath;
@override
void initState() {
super.initState();
_loadSettings();
}
// to load the saved settings from SharedPreferences when the page opens.
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_isBackupEnabled = prefs.getBool('isAutoBackupEnabled') ?? false;
_backupPath = prefs.getString('autoBackupPath');
});
}
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, style: getEnteTextTheme(context).largeBold),
content: TextField(
controller: textController,
autofocus: true,
obscureText: isPasswordHidden,
decoration: InputDecoration(
hintText: l10n.enterPassword,
hintStyle: getEnteTextTheme(context).mini,
suffixIcon: IconButton(
icon: Icon(
isPasswordHidden ? Icons.visibility_off : Icons.visibility,
),
onPressed: () {
setState(() {
isPasswordHidden = !isPasswordHidden;
});
},
),
),
onChanged: (text) => setState(() {}),
),
actions: [
Row(
children: [
Expanded(
child: ButtonWidget(
buttonType: ButtonType.secondary,
labelText: l10n.cancel,
onTap: () async => Navigator.of(context).pop(null),
),
),
const SizedBox(width: 8),
Expanded(
child: ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.saveAction,
isDisabled: textController.text.isEmpty,
onTap: () async => Navigator.of(context).pop(textController.text),
),
),
],
),
],
);
},
);
},
);
}
Future<ButtonResult?> _showLocationChoiceDialog({required String displayPath}) async {
final l10n = context.l10n;
final dialogBody =
'${l10n.backupLocationChoiceDescription}\n\nSelected: ${_simplifyPath(displayPath)}';
final result = await showDialogWidget(
title: l10n.chooseBackupLocation,
context: context,
body: dialogBody,
buttons: [
ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.saveBackup,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: l10n.changeLocation,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: l10n.cancel,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.cancel,
),
],
);
return result;
}
Future<bool> _handleLocationSetup() async {
String currentPath = _backupPath ?? await _getDefaultBackupPath();
while (true) {
final result = await _showLocationChoiceDialog(displayPath: currentPath);
if (result?.action == ButtonAction.first) {
final prefs = await SharedPreferences.getInstance();
try {
await Directory(currentPath).create(recursive: true);
await prefs.setString('autoBackupPath', currentPath);
setState(() {
_backupPath = currentPath;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.initialBackupCreated)),
);
return true;
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.noDefaultBackupFolder)),
);
return false;
}
}
else if (result?.action == ButtonAction.second) {
final newPath = await FilePicker.platform.getDirectoryPath();
if (newPath != null) {
currentPath = newPath;
}
}
else {
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';
}
String _simplifyPath(String fullPath) { //takes a file path string and shortens it if it matches the common Android root path.
const rootToRemove = '/storage/emulated/0/';
if (fullPath.startsWith(rootToRemove)) {
return fullPath.substring(rootToRemove.length);
}
return fullPath;
}
// opens directory picker
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();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(successMessage ?? l10n.locationUpdatedAndBackupCreated),
),
);
return true;
}
return false; //user cancelled the file picker
}
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 _handleLocationSetup();
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;
});
}
});
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.localBackupSettingsTitle), //text shown on appbar
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
l10n.enableAutomaticBackups, //toggle text
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;
});
}
},
),
],
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
l10n.backupDescription, //text below toggle
style: getEnteTextTheme(context).mini,
),
),
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: [
Text(
l10n.currentLocation, //shows current backup location
style: getEnteTextTheme(context).body,
),
const SizedBox(height: 4),
if (_backupPath != null)
Text(
_simplifyPath(_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(
_simplifyPath(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.changeCurrentLocation),
),
),
],
),
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, //security notice title
style: getEnteTextTheme(context)
.smallBold
.copyWith(
color: Colors.orange,
),
),
],
),
const SizedBox(height: 8),
Text(
l10n.backupSecurityNotice, //security notice description
style: getEnteTextTheme(context).mini.copyWith(
color: Colors.orange.shade700,
),
),
],
),
),
],
),
),
),
],
),
),
),
);
}
}

View File

@@ -414,6 +414,7 @@ Future<dynamic> showTextInputDialog(
bool alwaysShowSuccessState = false,
bool isPasswordInput = false,
bool useRootNavigator = false,
VoidCallback? onCancel,
}) {
return showDialog(
barrierColor: backdropFaintDark,

View File

@@ -1153,9 +1153,9 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "91e4d1a9c55b28bf93425d1f12faf410efc1e48d"
resolved-ref: "91e4d1a9c55b28bf93425d1f12faf410efc1e48d"
url: "https://github.com/Sayegh7/move_to_background"
ref: v2-only
resolved-ref: "0cdfeed654d79636eff0c57110f3f6ad5801ba2f"
url: "https://github.com/ente-io/move_to_background.git"
source: git
version: "1.0.2"
native_dio_adapter:

View File

@@ -95,10 +95,10 @@ dependencies:
local_auth_darwin: ^1.2.2
logging: ^1.0.1
modal_bottom_sheet: ^3.0.0
move_to_background: # no package updates on pub.dev
move_to_background: # no updates in git, replace package
git:
url: https://github.com/Sayegh7/move_to_background
ref: 91e4d1a9c55b28bf93425d1f12faf410efc1e48d
url: https://github.com/ente-io/move_to_background.git
ref: v2-only
native_dio_adapter: ^1.4.0
otp: ^3.1.1
package_info_plus: ^8.0.2