use new location

This commit is contained in:
Fabian Freund
2026-03-23 13:47:21 +01:00
parent 17cd456097
commit 296226760f
13 changed files with 404 additions and 125 deletions
@@ -22,9 +22,11 @@ import 'package:flutter/material.dart';
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:saf_util/saf_util.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/domain/entities/profile.dart';
import 'package:weblibre/features/user/domain/presentation/dialogs/password_confirmation_dialog.dart';
import 'package:weblibre/features/user/domain/providers/backup_directory.dart';
import 'package:weblibre/features/user/domain/services/user_backup.dart';
import 'package:weblibre/utils/ui_helper.dart';
@@ -156,6 +158,17 @@ class ProfileBackupScreen extends HookConsumerWidget {
}
}
if (ref.read(backupDirectoryUriProvider) == null) {
final dir = await SafUtil().pickDirectory(
writePermission: true,
persistablePermission: true,
);
if (dir == null) return;
ref
.read(backupDirectoryUriProvider.notifier)
.set(Uri.parse(dir.uri));
}
backupFuture.value = ref
.read(userBackupServiceProvider.notifier)
.createUserBackup(
@@ -18,11 +18,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart' as p;
import 'package:saf_util/saf_util.dart';
import 'package:weblibre/core/providers/format.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/user/domain/providers.dart';
import 'package:weblibre/features/user/domain/providers/backup_directory.dart';
import 'package:weblibre/features/user/domain/services/user_backup.dart';
import 'package:weblibre/presentation/widgets/failure_widget.dart';
@@ -33,67 +35,128 @@ final _filenamePattern = RegExp(
class ProfileBackupListScreen extends HookConsumerWidget {
const ProfileBackupListScreen({super.key});
Future<void> _pickDirectory(WidgetRef ref) async {
final dir = await SafUtil().pickDirectory(
writePermission: true,
persistablePermission: true,
);
if (dir != null) {
final dirUri = Uri.parse(dir.uri);
ref.read(backupDirectoryUriProvider.notifier).set(dirUri);
final migrated = await ref
.read(userBackupServiceProvider.notifier)
.migrateOldBackups(dirUri);
if (migrated > 0) {
ref.invalidate(backupListProvider);
}
}
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final dirUri = ref.watch(backupDirectoryUriProvider);
final backupListAsync = ref.watch(backupListProvider);
return Scaffold(
appBar: AppBar(title: const Text('Backups')),
body: SafeArea(
child: backupListAsync.when(
data: (backupList) {
return ListView.builder(
itemCount: backupList.length,
itemBuilder: (context, index) {
final file = backupList[index];
final match = _filenamePattern.firstMatch(
p.basename(file.path),
);
if (match != null) {
final profileName = match.group(1)!;
final datePart = match.group(2)!;
// Reparse into DateTime
final dateTime = UserBackupService.dateFormatter.decode(
datePart,
);
return ListTile(
key: ValueKey(file.path),
title: Text(profileName),
subtitle: Text(
ref.read(formatProvider.notifier).fullDateTime(dateTime),
),
onTap: () async {
await RestoreProfileRoute(
backupFilePath: file.path,
).push(context);
},
);
} else {
return ListTile(
key: ValueKey(file.path),
title: Text(p.basename(file.path)),
onTap: () async {
await RestoreProfileRoute(
backupFilePath: file.path,
).push(context);
},
);
}
},
);
},
error: (error, stackTrace) => FailureWidget(
title: 'Failed to get backups',
exception: error,
onRetry: () {
ref.invalidate(backupListProvider);
},
appBar: AppBar(
title: const Text('Backups'),
actions: [
IconButton(
icon: const Icon(MdiIcons.folderCog),
tooltip: 'Change backup directory',
onPressed: () => _pickDirectory(ref),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
],
),
body: SafeArea(
child: dirUri == null
? Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(MdiIcons.folderOpen, size: 64),
const SizedBox(height: 16),
const Text(
'Select a directory to store your backups.',
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const Text(
'Choose a location outside of the app to keep your backups safe across reinstalls.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
FilledButton.icon(
icon: const Icon(MdiIcons.folderPlus),
label: const Text('Select Backup Directory'),
onPressed: () => _pickDirectory(ref),
),
],
),
),
)
: backupListAsync.when(
data: (backupList) {
if (backupList.isEmpty) {
return const Center(child: Text('No backups found'));
}
return ListView.builder(
itemCount: backupList.length,
itemBuilder: (context, index) {
final file = backupList[index];
final match = _filenamePattern.firstMatch(file.name);
if (match != null) {
final profileName = match.group(1)!;
final datePart = match.group(2)!;
final dateTime = UserBackupService.dateFormatter.decode(
datePart,
);
return ListTile(
key: ValueKey(file.uri),
title: Text(profileName),
subtitle: Text(
ref
.read(formatProvider.notifier)
.fullDateTime(dateTime),
),
onTap: () async {
await RestoreProfileRoute(
backupFileUri: file.uri,
).push(context);
},
);
} else {
return ListTile(
key: ValueKey(file.uri),
title: Text(file.name),
onTap: () async {
await RestoreProfileRoute(
backupFileUri: file.uri,
).push(context);
},
);
}
},
);
},
error: (error, stackTrace) => FailureWidget(
title: 'Failed to get backups',
exception: error,
onRetry: () {
ref.invalidate(backupListProvider);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
);
}
@@ -17,8 +17,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
@@ -32,9 +30,9 @@ import 'package:weblibre/utils/ui_helper.dart';
enum RestoreTarget { createOrOverride, createNew }
class ProfileRestoreScreen extends HookConsumerWidget {
final File backupFile;
final Uri backupFileUri;
const ProfileRestoreScreen({super.key, required this.backupFile});
const ProfileRestoreScreen({super.key, required this.backupFileUri});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -149,7 +147,7 @@ class ProfileRestoreScreen extends HookConsumerWidget {
ref
.read(userBackupServiceProvider.notifier)
.restoreAndCreateOrOverride(
backupFile,
backupFileUri,
password: passwordTextController.text,
confirmOverrideCallback: () {
if (context.mounted) {
@@ -163,7 +161,7 @@ class ProfileRestoreScreen extends HookConsumerWidget {
ref
.read(userBackupServiceProvider.notifier)
.restoreAndCreateNew(
backupFile,
backupFileUri,
profileName: nameTextController.text,
password: passwordTextController.text,
),