option to restore from backup during onboarding
This commit is contained in:
@@ -18,4 +18,4 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
enum OnboardingMode { express, detailed }
|
enum OnboardingMode { express, detailed, restore }
|
||||||
|
|||||||
@@ -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/toolbar_layout.dart';
|
||||||
import 'package:weblibre/features/onboarding/presentation/pages/ublock_opt_in.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/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/onboarding.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/profile.dart';
|
||||||
|
import 'package:weblibre/utils/exit_app.dart';
|
||||||
|
|
||||||
class OnboardingScreen extends HookConsumerWidget {
|
class OnboardingScreen extends HookConsumerWidget {
|
||||||
final int currentRevision;
|
final int currentRevision;
|
||||||
@@ -66,6 +70,9 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
case 1:
|
case 1:
|
||||||
return [const AiConfigurationPage()];
|
return [const AiConfigurationPage()];
|
||||||
default:
|
default:
|
||||||
|
if (onboardingMode == OnboardingMode.restore && !isReturningUser) {
|
||||||
|
return [WelcomePage(isReturningUser: isReturningUser)];
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
WelcomePage(isReturningUser: isReturningUser),
|
WelcomePage(isReturningUser: isReturningUser),
|
||||||
const DefaultSearchPage(),
|
const DefaultSearchPage(),
|
||||||
@@ -78,7 +85,7 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
PermissionsPage(formKey: GlobalKey<FormState>()),
|
PermissionsPage(formKey: GlobalKey<FormState>()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}, [currentRevision, targetRevision, onboardingMode]);
|
}, [currentRevision, targetRevision, onboardingMode, isReturningUser]);
|
||||||
|
|
||||||
final lastPage = useRef(pageController.initialPage);
|
final lastPage = useRef(pageController.initialPage);
|
||||||
final currentPage = useState(pageController.initialPage);
|
final currentPage = useState(pageController.initialPage);
|
||||||
@@ -175,6 +182,63 @@ class OnboardingScreen extends HookConsumerWidget {
|
|||||||
label: const Text('Next'),
|
label: const Text('Next'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
else if (onboardingMode == OnboardingMode.restore &&
|
||||||
|
!isReturningUser)
|
||||||
|
Expanded(
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: eulaAccepted
|
||||||
|
? () async {
|
||||||
|
await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => ProfileBackupListScreen(
|
||||||
|
onBackupSelected: (context, uri) {
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
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
|
else
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ class WelcomePage extends ConsumerWidget {
|
|||||||
.read(onboardingModeProvider.notifier)
|
.read(onboardingModeProvider.notifier)
|
||||||
.select(OnboardingMode.detailed),
|
.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),
|
const SizedBox(height: 40),
|
||||||
_EulaCheckbox(
|
_EulaCheckbox(
|
||||||
|
|||||||
+18
-11
@@ -33,7 +33,20 @@ final _filenamePattern = RegExp(
|
|||||||
);
|
);
|
||||||
|
|
||||||
class ProfileBackupListScreen extends HookConsumerWidget {
|
class ProfileBackupListScreen extends HookConsumerWidget {
|
||||||
const ProfileBackupListScreen({super.key});
|
final void Function(BuildContext context, Uri backupFileUri)?
|
||||||
|
onBackupSelected;
|
||||||
|
|
||||||
|
const ProfileBackupListScreen({super.key, this.onBackupSelected});
|
||||||
|
|
||||||
|
Future<void> _handleSelection(BuildContext context, Uri backupFileUri) async {
|
||||||
|
if (onBackupSelected != null) {
|
||||||
|
onBackupSelected!(context, backupFileUri);
|
||||||
|
} else {
|
||||||
|
await RestoreProfileRoute(
|
||||||
|
backupFileUri: backupFileUri.toString(),
|
||||||
|
).push(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickDirectory(WidgetRef ref) async {
|
Future<void> _pickDirectory(WidgetRef ref) async {
|
||||||
final dir = await SafUtil().pickDirectory(
|
final dir = await SafUtil().pickDirectory(
|
||||||
@@ -128,21 +141,15 @@ class ProfileBackupListScreen extends HookConsumerWidget {
|
|||||||
.read(formatProvider.notifier)
|
.read(formatProvider.notifier)
|
||||||
.fullDateTime(dateTime),
|
.fullDateTime(dateTime),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () =>
|
||||||
await RestoreProfileRoute(
|
_handleSelection(context, Uri.parse(file.uri)),
|
||||||
backupFileUri: file.uri,
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
key: ValueKey(file.uri),
|
key: ValueKey(file.uri),
|
||||||
title: Text(file.name),
|
title: Text(file.name),
|
||||||
onTap: () async {
|
onTap: () =>
|
||||||
await RestoreProfileRoute(
|
_handleSelection(context, Uri.parse(file.uri)),
|
||||||
backupFileUri: file.uri,
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/routing/routes.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/presentation/dialogs/override_profile_dialog.dart';
|
||||||
import 'package:weblibre/features/user/domain/services/user_backup.dart';
|
import 'package:weblibre/features/user/domain/services/user_backup.dart';
|
||||||
import 'package:weblibre/utils/form_validators.dart';
|
import 'package:weblibre/utils/form_validators.dart';
|
||||||
@@ -31,8 +32,16 @@ enum RestoreTarget { createOrOverride, createNew }
|
|||||||
|
|
||||||
class ProfileRestoreScreen extends HookConsumerWidget {
|
class ProfileRestoreScreen extends HookConsumerWidget {
|
||||||
final Uri backupFileUri;
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -41,10 +50,10 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
final passwordTextController = useTextEditingController();
|
final passwordTextController = useTextEditingController();
|
||||||
final nameTextController = useTextEditingController();
|
final nameTextController = useTextEditingController();
|
||||||
|
|
||||||
final restoreFuture = useState<Future<bool>?>(null);
|
final restoreFuture = useState<Future<Profile?>?>(null);
|
||||||
final restoreState = useFuture(restoreFuture.value);
|
final restoreState = useFuture(restoreFuture.value);
|
||||||
|
|
||||||
final restoreTarget = useState(RestoreTarget.createNew);
|
final restoreTarget = useState(forcedTarget ?? RestoreTarget.createNew);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (restoreState.hasError) {
|
if (restoreState.hasError) {
|
||||||
@@ -54,7 +63,11 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
} else if (restoreState.hasData) {
|
} else if (restoreState.hasData) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
showInfoMessage(context, 'Backup restored successfully');
|
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),
|
const SizedBox(height: 16),
|
||||||
RadioGroup(
|
if (forcedTarget == null)
|
||||||
groupValue: restoreTarget.value,
|
RadioGroup(
|
||||||
onChanged: (value) {
|
groupValue: restoreTarget.value,
|
||||||
if (value != null) {
|
onChanged: (value) {
|
||||||
restoreTarget.value = value;
|
if (value != null) {
|
||||||
}
|
restoreTarget.value = value;
|
||||||
},
|
}
|
||||||
child: Column(
|
},
|
||||||
children: [
|
child: Column(
|
||||||
RadioListTile(
|
children: [
|
||||||
enabled: !disableInteraction,
|
RadioListTile(
|
||||||
value: RestoreTarget.createNew,
|
enabled: !disableInteraction,
|
||||||
title: const Text('Create New User'),
|
value: RestoreTarget.createNew,
|
||||||
subtitle: const Text('Restore backup as a new user'),
|
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',
|
|
||||||
),
|
),
|
||||||
),
|
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)
|
if (restoreTarget.value == RestoreTarget.createNew)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: nameTextController,
|
controller: nameTextController,
|
||||||
@@ -156,7 +170,8 @@ class ProfileRestoreScreen extends HookConsumerWidget {
|
|||||||
throw Exception('Override failed');
|
throw Exception('Override failed');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
.then<Profile?>((_) => null),
|
||||||
RestoreTarget.createNew =>
|
RestoreTarget.createNew =>
|
||||||
ref
|
ref
|
||||||
.read(userBackupServiceProvider.notifier)
|
.read(userBackupServiceProvider.notifier)
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class UserBackupService extends _$UserBackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> restoreAndCreateNew(
|
Future<Profile> restoreAndCreateNew(
|
||||||
Uri backupFileUri, {
|
Uri backupFileUri, {
|
||||||
required String profileName,
|
required String profileName,
|
||||||
required String password,
|
required String password,
|
||||||
@@ -222,17 +222,18 @@ class UserBackupService extends _$UserBackupService {
|
|||||||
outputDirectory: outputDirectory,
|
outputDirectory: outputDirectory,
|
||||||
argon2Params: Argon2Params.memoryConstrained(),
|
argon2Params: Argon2Params.memoryConstrained(),
|
||||||
);
|
);
|
||||||
await backup.unpack(password).then((_) async {
|
final newProfile = await backup.unpack(password).then((_) async {
|
||||||
final newProfile = Profile.create(name: profileName);
|
final newProfile = Profile.create(name: profileName);
|
||||||
final newPath = filesystem.getProfileDir(newProfile.uuidValue);
|
final newPath = filesystem.getProfileDir(newProfile.uuidValue);
|
||||||
|
|
||||||
await outputDirectory.rename(newPath.path);
|
await outputDirectory.rename(newPath.path);
|
||||||
await filesystem.updateProfileMetadata(newProfile);
|
await filesystem.updateProfileMetadata(newProfile);
|
||||||
await filesystem.healProfile(newPath);
|
await filesystem.healProfile(newPath);
|
||||||
|
return newProfile;
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.invalidate(profileRepositoryProvider);
|
ref.invalidate(profileRepositoryProvider);
|
||||||
return true;
|
return newProfile;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
if (await tempFile.exists()) {
|
if (await tempFile.exists()) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class UserBackupServiceProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$userBackupServiceHash() => r'3746ae59d490e25a44815c125f30cd0981de555d';
|
String _$userBackupServiceHash() => r'52c7d3df3ce2d603692a035c77987727b840a359';
|
||||||
|
|
||||||
abstract class _$UserBackupService extends $Notifier<void> {
|
abstract class _$UserBackupService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ dependencies:
|
|||||||
rss_dart: ^1.0.14
|
rss_dart: ^1.0.14
|
||||||
rxdart: ^0.28.0
|
rxdart: ^0.28.0
|
||||||
saf_stream: ^2.0.0
|
saf_stream: ^2.0.0
|
||||||
saf_util: ^2.0.0
|
saf_util: ^2.1.0
|
||||||
secure_archive:
|
secure_archive:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/FaFre/secure_archive.git
|
url: https://github.com/FaFre/secure_archive.git
|
||||||
|
|||||||
Reference in New Issue
Block a user