use new location
This commit is contained in:
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user