Skip to content

Commit 5e6cc7a

Browse files
committed
option to restore from backup during onboarding
1 parent dcb0c8d commit 5e6cc7a

8 files changed

Lines changed: 145 additions & 47 deletions

File tree

apps/weblibre/lib/features/onboarding/domain/entities/onboarding_mode.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1919
*/
2020

21-
enum OnboardingMode { express, detailed }
21+
enum OnboardingMode { express, detailed, restore }

apps/weblibre/lib/features/onboarding/presentation/onboarding.dart

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ import 'package:weblibre/features/onboarding/presentation/pages/privacy_hardenin
3636
import 'package:weblibre/features/onboarding/presentation/pages/toolbar_layout.dart';
3737
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.dart';
3838
import 'package:weblibre/features/onboarding/presentation/pages/welcome.dart';
39+
import 'package:weblibre/features/user/domain/presentation/screens/profile_backup_list.dart';
40+
import 'package:weblibre/features/user/domain/presentation/screens/profile_restore.dart';
3941
import 'package:weblibre/features/user/domain/repositories/onboarding.dart';
42+
import 'package:weblibre/features/user/domain/repositories/profile.dart';
43+
import 'package:weblibre/utils/exit_app.dart';
4044

4145
class OnboardingScreen extends HookConsumerWidget {
4246
final int currentRevision;
@@ -66,6 +70,9 @@ class OnboardingScreen extends HookConsumerWidget {
6670
case 1:
6771
return [const AiConfigurationPage()];
6872
default:
73+
if (onboardingMode == OnboardingMode.restore && !isReturningUser) {
74+
return [WelcomePage(isReturningUser: isReturningUser)];
75+
}
6976
return [
7077
WelcomePage(isReturningUser: isReturningUser),
7178
const DefaultSearchPage(),
@@ -78,7 +85,7 @@ class OnboardingScreen extends HookConsumerWidget {
7885
PermissionsPage(formKey: GlobalKey<FormState>()),
7986
];
8087
}
81-
}, [currentRevision, targetRevision, onboardingMode]);
88+
}, [currentRevision, targetRevision, onboardingMode, isReturningUser]);
8289

8390
final lastPage = useRef(pageController.initialPage);
8491
final currentPage = useState(pageController.initialPage);
@@ -175,6 +182,63 @@ class OnboardingScreen extends HookConsumerWidget {
175182
label: const Text('Next'),
176183
),
177184
)
185+
else if (onboardingMode == OnboardingMode.restore &&
186+
!isReturningUser)
187+
Expanded(
188+
child: TextButton.icon(
189+
onPressed: eulaAccepted
190+
? () async {
191+
await Navigator.of(context).push(
192+
MaterialPageRoute<void>(
193+
builder: (_) => ProfileBackupListScreen(
194+
onBackupSelected: (context, uri) {
195+
unawaited(
196+
Navigator.of(context).push(
197+
MaterialPageRoute<void>(
198+
builder: (_) =>
199+
ProfileRestoreScreen(
200+
backupFileUri: uri,
201+
forcedTarget:
202+
RestoreTarget.createNew,
203+
onRestoreSuccess:
204+
(_, profile) async {
205+
await ref
206+
.read(
207+
onboardingRepositoryProvider
208+
.notifier,
209+
)
210+
.pushRevision(
211+
targetRevision,
212+
);
213+
if (profile != null) {
214+
await ref
215+
.read(
216+
profileRepositoryProvider
217+
.notifier,
218+
)
219+
.switchProfile(
220+
profile.id,
221+
);
222+
await exitApp(
223+
ref.container,
224+
);
225+
}
226+
},
227+
),
228+
),
229+
),
230+
);
231+
},
232+
),
233+
),
234+
);
235+
}
236+
: null,
237+
iconAlignment: IconAlignment.end,
238+
icon: const Icon(Icons.settings_backup_restore),
239+
label: const Text('Restore'),
240+
),
241+
)
178242
else
179243
Expanded(
180244
child: TextButton.icon(

apps/weblibre/lib/features/onboarding/presentation/pages/welcome.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ class WelcomePage extends ConsumerWidget {
9191
.read(onboardingModeProvider.notifier)
9292
.select(OnboardingMode.detailed),
9393
),
94+
const SizedBox(height: 12),
95+
_ModeOption(
96+
mode: OnboardingMode.restore,
97+
title: 'Restore from Backup',
98+
subtitle: 'Import a profile from an encrypted backup file.',
99+
icon: Icons.settings_backup_restore,
100+
selected: selectedMode == OnboardingMode.restore,
101+
onTap: () => ref
102+
.read(onboardingModeProvider.notifier)
103+
.select(OnboardingMode.restore),
104+
),
94105
],
95106
const SizedBox(height: 40),
96107
_EulaCheckbox(

apps/weblibre/lib/features/user/domain/presentation/screens/profile_backup_list.dart

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,20 @@ final _filenamePattern = RegExp(
3333
);
3434

3535
class ProfileBackupListScreen extends HookConsumerWidget {
36-
const ProfileBackupListScreen({super.key});
36+
final void Function(BuildContext context, Uri backupFileUri)?
37+
onBackupSelected;
38+
39+
const ProfileBackupListScreen({super.key, this.onBackupSelected});
40+
41+
Future<void> _handleSelection(BuildContext context, Uri backupFileUri) async {
42+
if (onBackupSelected != null) {
43+
onBackupSelected!(context, backupFileUri);
44+
} else {
45+
await RestoreProfileRoute(
46+
backupFileUri: backupFileUri.toString(),
47+
).push(context);
48+
}
49+
}
3750

3851
Future<void> _pickDirectory(WidgetRef ref) async {
3952
final dir = await SafUtil().pickDirectory(
@@ -128,21 +141,15 @@ class ProfileBackupListScreen extends HookConsumerWidget {
128141
.read(formatProvider.notifier)
129142
.fullDateTime(dateTime),
130143
),
131-
onTap: () async {
132-
await RestoreProfileRoute(
133-
backupFileUri: file.uri,
134-
).push(context);
135-
},
144+
onTap: () =>
145+
_handleSelection(context, Uri.parse(file.uri)),
136146
);
137147
} else {
138148
return ListTile(
139149
key: ValueKey(file.uri),
140150
title: Text(file.name),
141-
onTap: () async {
142-
await RestoreProfileRoute(
143-
backupFileUri: file.uri,
144-
).push(context);
145-
},
151+
onTap: () =>
152+
_handleSelection(context, Uri.parse(file.uri)),
146153
);
147154
}
148155
},

apps/weblibre/lib/features/user/domain/presentation/screens/profile_restore.dart

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
2222
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
2323
import 'package:hooks_riverpod/hooks_riverpod.dart';
2424
import 'package:weblibre/core/routing/routes.dart';
25+
import 'package:weblibre/domain/entities/profile.dart';
2526
import 'package:weblibre/features/user/domain/presentation/dialogs/override_profile_dialog.dart';
2627
import 'package:weblibre/features/user/domain/services/user_backup.dart';
2728
import 'package:weblibre/utils/form_validators.dart';
@@ -31,8 +32,16 @@ enum RestoreTarget { createOrOverride, createNew }
3132

3233
class ProfileRestoreScreen extends HookConsumerWidget {
3334
final Uri backupFileUri;
35+
final RestoreTarget? forcedTarget;
36+
final void Function(BuildContext context, Profile? restoredProfile)?
37+
onRestoreSuccess;
3438

35-
const ProfileRestoreScreen({super.key, required this.backupFileUri});
39+
const ProfileRestoreScreen({
40+
super.key,
41+
required this.backupFileUri,
42+
this.forcedTarget,
43+
this.onRestoreSuccess,
44+
});
3645

3746
@override
3847
Widget build(BuildContext context, WidgetRef ref) {
@@ -41,10 +50,10 @@ class ProfileRestoreScreen extends HookConsumerWidget {
4150
final passwordTextController = useTextEditingController();
4251
final nameTextController = useTextEditingController();
4352

44-
final restoreFuture = useState<Future<bool>?>(null);
53+
final restoreFuture = useState<Future<Profile?>?>(null);
4554
final restoreState = useFuture(restoreFuture.value);
4655

47-
final restoreTarget = useState(RestoreTarget.createNew);
56+
final restoreTarget = useState(forcedTarget ?? RestoreTarget.createNew);
4857

4958
useEffect(() {
5059
if (restoreState.hasError) {
@@ -54,7 +63,11 @@ class ProfileRestoreScreen extends HookConsumerWidget {
5463
} else if (restoreState.hasData) {
5564
WidgetsBinding.instance.addPostFrameCallback((_) {
5665
showInfoMessage(context, 'Backup restored successfully');
57-
ProfileListRoute().go(context);
66+
if (onRestoreSuccess != null) {
67+
onRestoreSuccess!(context, restoreState.data);
68+
} else {
69+
ProfileListRoute().go(context);
70+
}
5871
});
5972
}
6073

@@ -93,32 +106,33 @@ class ProfileRestoreScreen extends HookConsumerWidget {
93106
},
94107
),
95108
const SizedBox(height: 16),
96-
RadioGroup(
97-
groupValue: restoreTarget.value,
98-
onChanged: (value) {
99-
if (value != null) {
100-
restoreTarget.value = value;
101-
}
102-
},
103-
child: Column(
104-
children: [
105-
RadioListTile(
106-
enabled: !disableInteraction,
107-
value: RestoreTarget.createNew,
108-
title: const Text('Create New User'),
109-
subtitle: const Text('Restore backup as a new user'),
110-
),
111-
RadioListTile(
112-
enabled: !disableInteraction,
113-
value: RestoreTarget.createOrOverride,
114-
title: const Text('Restore & Replace'),
115-
subtitle: const Text(
116-
'Restore backup and overwrite existing user if present',
109+
if (forcedTarget == null)
110+
RadioGroup(
111+
groupValue: restoreTarget.value,
112+
onChanged: (value) {
113+
if (value != null) {
114+
restoreTarget.value = value;
115+
}
116+
},
117+
child: Column(
118+
children: [
119+
RadioListTile(
120+
enabled: !disableInteraction,
121+
value: RestoreTarget.createNew,
122+
title: const Text('Create New User'),
123+
subtitle: const Text('Restore backup as a new user'),
117124
),
118-
),
119-
],
125+
RadioListTile(
126+
enabled: !disableInteraction,
127+
value: RestoreTarget.createOrOverride,
128+
title: const Text('Restore & Replace'),
129+
subtitle: const Text(
130+
'Restore backup and overwrite existing user if present',
131+
),
132+
),
133+
],
134+
),
120135
),
121-
),
122136
if (restoreTarget.value == RestoreTarget.createNew)
123137
TextFormField(
124138
controller: nameTextController,
@@ -156,7 +170,8 @@ class ProfileRestoreScreen extends HookConsumerWidget {
156170
throw Exception('Override failed');
157171
}
158172
},
159-
),
173+
)
174+
.then<Profile?>((_) => null),
160175
RestoreTarget.createNew =>
161176
ref
162177
.read(userBackupServiceProvider.notifier)

apps/weblibre/lib/features/user/domain/services/user_backup.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ class UserBackupService extends _$UserBackupService {
202202
}
203203
}
204204

205-
Future<bool> restoreAndCreateNew(
205+
Future<Profile> restoreAndCreateNew(
206206
Uri backupFileUri, {
207207
required String profileName,
208208
required String password,
@@ -222,17 +222,18 @@ class UserBackupService extends _$UserBackupService {
222222
outputDirectory: outputDirectory,
223223
argon2Params: Argon2Params.memoryConstrained(),
224224
);
225-
await backup.unpack(password).then((_) async {
225+
final newProfile = await backup.unpack(password).then((_) async {
226226
final newProfile = Profile.create(name: profileName);
227227
final newPath = filesystem.getProfileDir(newProfile.uuidValue);
228228

229229
await outputDirectory.rename(newPath.path);
230230
await filesystem.updateProfileMetadata(newProfile);
231231
await filesystem.healProfile(newPath);
232+
return newProfile;
232233
});
233234

234235
ref.invalidate(profileRepositoryProvider);
235-
return true;
236+
return newProfile;
236237
} finally {
237238
try {
238239
if (await tempFile.exists()) {

apps/weblibre/lib/features/user/domain/services/user_backup.g.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/weblibre/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ dependencies:
7171
rss_dart: ^1.0.14
7272
rxdart: ^0.28.0
7373
saf_stream: ^2.0.0
74-
saf_util: ^2.0.0
74+
saf_util: ^2.1.0
7575
secure_archive:
7676
git:
7777
url: https://github.com/FaFre/secure_archive.git

0 commit comments

Comments
 (0)