diff --git a/apps/weblibre/lib/features/onboarding/domain/entities/onboarding_mode.dart b/apps/weblibre/lib/features/onboarding/domain/entities/onboarding_mode.dart
index 9c0ae2cd..18c6e7b2 100644
--- a/apps/weblibre/lib/features/onboarding/domain/entities/onboarding_mode.dart
+++ b/apps/weblibre/lib/features/onboarding/domain/entities/onboarding_mode.dart
@@ -18,4 +18,4 @@
* along with this program. If not, see .
*/
-enum OnboardingMode { express, detailed }
+enum OnboardingMode { express, detailed, restore }
diff --git a/apps/weblibre/lib/features/onboarding/presentation/onboarding.dart b/apps/weblibre/lib/features/onboarding/presentation/onboarding.dart
index 48aa0a69..2e21d88b 100644
--- a/apps/weblibre/lib/features/onboarding/presentation/onboarding.dart
+++ b/apps/weblibre/lib/features/onboarding/presentation/onboarding.dart
@@ -36,7 +36,11 @@ import 'package:weblibre/features/onboarding/presentation/pages/privacy_hardenin
import 'package:weblibre/features/onboarding/presentation/pages/toolbar_layout.dart';
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.dart';
import 'package:weblibre/features/onboarding/presentation/pages/welcome.dart';
+import 'package:weblibre/features/user/domain/presentation/screens/profile_backup_list.dart';
+import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
+import 'package:weblibre/features/user/domain/repositories/profile.dart';
+import 'package:weblibre/utils/exit_app.dart';
class OnboardingScreen extends HookConsumerWidget {
final int currentRevision;
@@ -66,6 +70,9 @@ class OnboardingScreen extends HookConsumerWidget {
case 1:
return [const AiConfigurationPage()];
default:
+ if (onboardingMode == OnboardingMode.restore && !isReturningUser) {
+ return [WelcomePage(isReturningUser: isReturningUser)];
+ }
return [
WelcomePage(isReturningUser: isReturningUser),
const DefaultSearchPage(),
@@ -78,7 +85,7 @@ class OnboardingScreen extends HookConsumerWidget {
PermissionsPage(formKey: GlobalKey()),
];
}
- }, [currentRevision, targetRevision, onboardingMode]);
+ }, [currentRevision, targetRevision, onboardingMode, isReturningUser]);
final lastPage = useRef(pageController.initialPage);
final currentPage = useState(pageController.initialPage);
@@ -175,6 +182,63 @@ class OnboardingScreen extends HookConsumerWidget {
label: const Text('Next'),
),
)
+ else if (onboardingMode == OnboardingMode.restore &&
+ !isReturningUser)
+ Expanded(
+ child: TextButton.icon(
+ onPressed: eulaAccepted
+ ? () async {
+ await Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => ProfileBackupListScreen(
+ onBackupSelected: (context, uri) {
+ unawaited(
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) =>
+ ProfileRestoreScreen(
+ backupFileUri: uri,
+ forcedTarget:
+ RestoreTarget.createNew,
+ onRestoreSuccess:
+ (_, profile) async {
+ await ref
+ .read(
+ onboardingRepositoryProvider
+ .notifier,
+ )
+ .pushRevision(
+ targetRevision,
+ );
+ if (profile != null) {
+ await ref
+ .read(
+ profileRepositoryProvider
+ .notifier,
+ )
+ .switchProfile(
+ profile.id,
+ );
+ await exitApp(
+ ref.container,
+ );
+ }
+ },
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+ : null,
+ iconAlignment: IconAlignment.end,
+ icon: const Icon(Icons.settings_backup_restore),
+ label: const Text('Restore'),
+ ),
+ )
else
Expanded(
child: TextButton.icon(
diff --git a/apps/weblibre/lib/features/onboarding/presentation/pages/welcome.dart b/apps/weblibre/lib/features/onboarding/presentation/pages/welcome.dart
index 9e3443a3..40ce0385 100644
--- a/apps/weblibre/lib/features/onboarding/presentation/pages/welcome.dart
+++ b/apps/weblibre/lib/features/onboarding/presentation/pages/welcome.dart
@@ -91,6 +91,17 @@ class WelcomePage extends ConsumerWidget {
.read(onboardingModeProvider.notifier)
.select(OnboardingMode.detailed),
),
+ const SizedBox(height: 12),
+ _ModeOption(
+ mode: OnboardingMode.restore,
+ title: 'Restore from Backup',
+ subtitle: 'Import a profile from an encrypted backup file.',
+ icon: Icons.settings_backup_restore,
+ selected: selectedMode == OnboardingMode.restore,
+ onTap: () => ref
+ .read(onboardingModeProvider.notifier)
+ .select(OnboardingMode.restore),
+ ),
],
const SizedBox(height: 40),
_EulaCheckbox(
diff --git a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup_list.dart b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup_list.dart
index 46863d32..acc010e6 100644
--- a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup_list.dart
+++ b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup_list.dart
@@ -33,7 +33,20 @@ final _filenamePattern = RegExp(
);
class ProfileBackupListScreen extends HookConsumerWidget {
- const ProfileBackupListScreen({super.key});
+ final void Function(BuildContext context, Uri backupFileUri)?
+ onBackupSelected;
+
+ const ProfileBackupListScreen({super.key, this.onBackupSelected});
+
+ Future _handleSelection(BuildContext context, Uri backupFileUri) async {
+ if (onBackupSelected != null) {
+ onBackupSelected!(context, backupFileUri);
+ } else {
+ await RestoreProfileRoute(
+ backupFileUri: backupFileUri.toString(),
+ ).push(context);
+ }
+ }
Future _pickDirectory(WidgetRef ref) async {
final dir = await SafUtil().pickDirectory(
@@ -128,21 +141,15 @@ class ProfileBackupListScreen extends HookConsumerWidget {
.read(formatProvider.notifier)
.fullDateTime(dateTime),
),
- onTap: () async {
- await RestoreProfileRoute(
- backupFileUri: file.uri,
- ).push(context);
- },
+ onTap: () =>
+ _handleSelection(context, Uri.parse(file.uri)),
);
} else {
return ListTile(
key: ValueKey(file.uri),
title: Text(file.name),
- onTap: () async {
- await RestoreProfileRoute(
- backupFileUri: file.uri,
- ).push(context);
- },
+ onTap: () =>
+ _handleSelection(context, Uri.parse(file.uri)),
);
}
},
diff --git a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart
index 539581f2..7407c0e8 100644
--- a/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart
+++ b/apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart
@@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
+import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/features/user/domain/presentation/dialogs/override_profile_dialog.dart';
import 'package:weblibre/features/user/domain/services/user_backup.dart';
import 'package:weblibre/utils/form_validators.dart';
@@ -31,8 +32,16 @@ enum RestoreTarget { createOrOverride, createNew }
class ProfileRestoreScreen extends HookConsumerWidget {
final Uri backupFileUri;
+ final RestoreTarget? forcedTarget;
+ final void Function(BuildContext context, Profile? restoredProfile)?
+ onRestoreSuccess;
- const ProfileRestoreScreen({super.key, required this.backupFileUri});
+ const ProfileRestoreScreen({
+ super.key,
+ required this.backupFileUri,
+ this.forcedTarget,
+ this.onRestoreSuccess,
+ });
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -41,10 +50,10 @@ class ProfileRestoreScreen extends HookConsumerWidget {
final passwordTextController = useTextEditingController();
final nameTextController = useTextEditingController();
- final restoreFuture = useState?>(null);
+ final restoreFuture = useState?>(null);
final restoreState = useFuture(restoreFuture.value);
- final restoreTarget = useState(RestoreTarget.createNew);
+ final restoreTarget = useState(forcedTarget ?? RestoreTarget.createNew);
useEffect(() {
if (restoreState.hasError) {
@@ -54,7 +63,11 @@ class ProfileRestoreScreen extends HookConsumerWidget {
} else if (restoreState.hasData) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showInfoMessage(context, 'Backup restored successfully');
- ProfileListRoute().go(context);
+ if (onRestoreSuccess != null) {
+ onRestoreSuccess!(context, restoreState.data);
+ } else {
+ ProfileListRoute().go(context);
+ }
});
}
@@ -93,32 +106,33 @@ class ProfileRestoreScreen extends HookConsumerWidget {
},
),
const SizedBox(height: 16),
- RadioGroup(
- groupValue: restoreTarget.value,
- onChanged: (value) {
- if (value != null) {
- restoreTarget.value = value;
- }
- },
- child: Column(
- children: [
- RadioListTile(
- enabled: !disableInteraction,
- value: RestoreTarget.createNew,
- title: const Text('Create New User'),
- subtitle: const Text('Restore backup as a new user'),
- ),
- RadioListTile(
- enabled: !disableInteraction,
- value: RestoreTarget.createOrOverride,
- title: const Text('Restore & Replace'),
- subtitle: const Text(
- 'Restore backup and overwrite existing user if present',
+ if (forcedTarget == null)
+ RadioGroup(
+ groupValue: restoreTarget.value,
+ onChanged: (value) {
+ if (value != null) {
+ restoreTarget.value = value;
+ }
+ },
+ child: Column(
+ children: [
+ RadioListTile(
+ enabled: !disableInteraction,
+ value: RestoreTarget.createNew,
+ title: const Text('Create New User'),
+ subtitle: const Text('Restore backup as a new user'),
),
- ),
- ],
+ RadioListTile(
+ enabled: !disableInteraction,
+ value: RestoreTarget.createOrOverride,
+ title: const Text('Restore & Replace'),
+ subtitle: const Text(
+ 'Restore backup and overwrite existing user if present',
+ ),
+ ),
+ ],
+ ),
),
- ),
if (restoreTarget.value == RestoreTarget.createNew)
TextFormField(
controller: nameTextController,
@@ -156,7 +170,8 @@ class ProfileRestoreScreen extends HookConsumerWidget {
throw Exception('Override failed');
}
},
- ),
+ )
+ .then((_) => null),
RestoreTarget.createNew =>
ref
.read(userBackupServiceProvider.notifier)
diff --git a/apps/weblibre/lib/features/user/domain/services/user_backup.dart b/apps/weblibre/lib/features/user/domain/services/user_backup.dart
index 94f8a8c8..e3188566 100644
--- a/apps/weblibre/lib/features/user/domain/services/user_backup.dart
+++ b/apps/weblibre/lib/features/user/domain/services/user_backup.dart
@@ -202,7 +202,7 @@ class UserBackupService extends _$UserBackupService {
}
}
- Future restoreAndCreateNew(
+ Future restoreAndCreateNew(
Uri backupFileUri, {
required String profileName,
required String password,
@@ -222,17 +222,18 @@ class UserBackupService extends _$UserBackupService {
outputDirectory: outputDirectory,
argon2Params: Argon2Params.memoryConstrained(),
);
- await backup.unpack(password).then((_) async {
+ final newProfile = await backup.unpack(password).then((_) async {
final newProfile = Profile.create(name: profileName);
final newPath = filesystem.getProfileDir(newProfile.uuidValue);
await outputDirectory.rename(newPath.path);
await filesystem.updateProfileMetadata(newProfile);
await filesystem.healProfile(newPath);
+ return newProfile;
});
ref.invalidate(profileRepositoryProvider);
- return true;
+ return newProfile;
} finally {
try {
if (await tempFile.exists()) {
diff --git a/apps/weblibre/lib/features/user/domain/services/user_backup.g.dart b/apps/weblibre/lib/features/user/domain/services/user_backup.g.dart
index 06600ab3..eb45b06b 100644
--- a/apps/weblibre/lib/features/user/domain/services/user_backup.g.dart
+++ b/apps/weblibre/lib/features/user/domain/services/user_backup.g.dart
@@ -41,7 +41,7 @@ final class UserBackupServiceProvider
}
}
-String _$userBackupServiceHash() => r'3746ae59d490e25a44815c125f30cd0981de555d';
+String _$userBackupServiceHash() => r'52c7d3df3ce2d603692a035c77987727b840a359';
abstract class _$UserBackupService extends $Notifier {
void build();
diff --git a/apps/weblibre/pubspec.yaml b/apps/weblibre/pubspec.yaml
index 44222d96..36f10f55 100644
--- a/apps/weblibre/pubspec.yaml
+++ b/apps/weblibre/pubspec.yaml
@@ -71,7 +71,7 @@ dependencies:
rss_dart: ^1.0.14
rxdart: ^0.28.0
saf_stream: ^2.0.0
- saf_util: ^2.0.0
+ saf_util: ^2.1.0
secure_archive:
git:
url: https://github.com/FaFre/secure_archive.git