Skip to content

Commit b0e3297

Browse files
mikebarkminCopilot
andcommitted
feat(settings): show inline backup list with restore per entry
Replace single restore-backup button with an inline list of all available WebDAV backups. Each entry shows library name, modified date and file size. A refresh icon reloads the list. Tapping the restore icon on an entry calls _restoreFromBackup with a confirm dialog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent af1e52a commit b0e3297

1 file changed

Lines changed: 146 additions & 20 deletions

File tree

lib/features/settings/settings_screen.dart

Lines changed: 146 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import 'package:package_info_plus/package_info_plus.dart';
88
import '../../core/providers/app_providers.dart';
99
import '../../core/security/security_preferences_service.dart';
1010
import '../../core/session/app_session_controller.dart';
11+
import '../../core/storage/library_backup_service.dart';
1112
import '../../core/update/app_update_controller.dart';
1213
import '../../shared/utils/formatting.dart';
1314
import '../../shared/widgets/app_updater.dart';
1415
import '../../shared/widgets/app_error_state.dart';
1516
import '../../shared/widgets/content_constraints.dart';
1617
import '../setup/database_selection_sheet.dart';
17-
import '../setup/webdav_restore_flow.dart';
1818
import 'grade_system_controller.dart';
1919
import 'grade_system_editor.dart';
2020
import '../students/student_sorting.dart';
@@ -670,13 +670,19 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
670670
bool? _connectionOk;
671671
bool _isExportingNow = false;
672672
bool _isRestoringNow = false;
673+
List<WebDavBackupEntry>? _backups;
674+
bool _loadingBackups = false;
675+
bool _backupsLoadFailed = false;
673676

674677
@override
675678
void initState() {
676679
super.initState();
677680
_urlController.text = widget.session.webDavUrl ?? '';
678681
_usernameController.text = widget.session.webDavUsername ?? '';
679682
_serverPathController.text = widget.session.webDavServerPath ?? '/';
683+
if (widget.session.isWebDavConfigured) {
684+
WidgetsBinding.instance.addPostFrameCallback((_) => _loadBackups());
685+
}
680686
}
681687

682688
@override
@@ -697,7 +703,10 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
697703
}
698704
final path = _serverPathController.text.trim();
699705
await session.setWebDavServerPath(path.isEmpty ? '/' : path);
700-
if (mounted) setState(() => _connectionOk = null);
706+
if (mounted) {
707+
setState(() => _connectionOk = null);
708+
_loadBackups();
709+
}
701710
}
702711

703712
Future<void> _testConnection() async {
@@ -722,6 +731,22 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
722731
}
723732
}
724733

734+
Future<void> _loadBackups() async {
735+
if (_loadingBackups) return;
736+
setState(() {
737+
_loadingBackups = true;
738+
_backupsLoadFailed = false;
739+
});
740+
try {
741+
final backups = await ref.read(appSessionProvider).listWebDavBackups();
742+
if (mounted) setState(() => _backups = backups);
743+
} catch (_) {
744+
if (mounted) setState(() => _backupsLoadFailed = true);
745+
} finally {
746+
if (mounted) setState(() => _loadingBackups = false);
747+
}
748+
}
749+
725750
Future<void> _exportNow() async {
726751
setState(() => _isExportingNow = true);
727752
try {
@@ -739,7 +764,7 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
739764
}
740765
}
741766

742-
Future<void> _restoreNow() async {
767+
Future<void> _restoreFromBackup(WebDavBackupEntry backup) async {
743768
final confirmed = await showDialog<bool>(
744769
context: context,
745770
builder: (ctx) => AlertDialog(
@@ -763,11 +788,16 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
763788
try {
764789
final dbPath = await widget.session.currentDatabasePath();
765790
if (!mounted) return;
766-
await restoreWebDavBackupFlow(
767-
context: context,
768-
ref: ref,
769-
destinationPath: dbPath,
770-
createNew: false,
791+
final errorCode = await ref
792+
.read(appSessionProvider)
793+
.restoreWebDavBackup(
794+
remotePath: backup.remotePath,
795+
destinationPath: dbPath,
796+
createNew: false,
797+
);
798+
if (!mounted) return;
799+
ScaffoldMessenger.of(context).showSnackBar(
800+
SnackBar(content: Text((errorCode ?? 'backup_restored').tr())),
771801
);
772802
} finally {
773803
if (mounted) setState(() => _isRestoringNow = false);
@@ -917,20 +947,61 @@ class _BackupsSectionState extends ConsumerState<_BackupsSection> {
917947
),
918948
],
919949
if (isConfigured) ...[
950+
const SizedBox(height: 16),
951+
const Divider(height: 1),
920952
const SizedBox(height: 12),
921-
Align(
922-
alignment: Alignment.centerRight,
923-
child: OutlinedButton.icon(
924-
onPressed: _isRestoringNow ? null : _restoreNow,
925-
icon: _isRestoringNow
926-
? const SizedBox.square(
927-
dimension: 16,
928-
child: CircularProgressIndicator(strokeWidth: 2),
929-
)
930-
: const Icon(Icons.cloud_download_outlined),
931-
label: Text('restore_backup'.tr()),
932-
),
953+
Row(
954+
children: [
955+
Expanded(
956+
child: Text(
957+
'available_backups'.tr(),
958+
style: Theme.of(context).textTheme.labelLarge,
959+
),
960+
),
961+
IconButton(
962+
visualDensity: VisualDensity.compact,
963+
onPressed: _loadingBackups ? null : _loadBackups,
964+
icon: _loadingBackups
965+
? const SizedBox.square(
966+
dimension: 16,
967+
child: CircularProgressIndicator(strokeWidth: 2),
968+
)
969+
: const Icon(Icons.refresh),
970+
tooltip: 'retry'.tr(),
971+
),
972+
],
933973
),
974+
const SizedBox(height: 8),
975+
if (_backupsLoadFailed)
976+
Text(
977+
'webdav_connection_failed'.tr(),
978+
style: TextStyle(
979+
color: Theme.of(context).colorScheme.error,
980+
),
981+
)
982+
else if (_backups == null || _loadingBackups && _backups!.isEmpty)
983+
const SizedBox.shrink()
984+
else if (_backups!.isEmpty)
985+
Text(
986+
'no_webdav_backups_found'.tr(),
987+
style: Theme.of(context).textTheme.bodySmall,
988+
)
989+
else
990+
ListView.separated(
991+
shrinkWrap: true,
992+
physics: const NeverScrollableScrollPhysics(),
993+
itemCount: _backups!.length,
994+
separatorBuilder: (_, _) => const Divider(height: 1),
995+
itemBuilder: (context, index) {
996+
final backup = _backups![index];
997+
return _BackupListTile(
998+
backup: backup,
999+
localeTag: localeTag,
1000+
restoring: _isRestoringNow,
1001+
onRestore: () => _restoreFromBackup(backup),
1002+
);
1003+
},
1004+
),
9341005
],
9351006
if (session.hasPendingAutoImport) ...[
9361007
const SizedBox(height: 8),
@@ -1026,3 +1097,58 @@ class _MaxVersionsPicker extends StatelessWidget {
10261097
);
10271098
}
10281099
}
1100+
1101+
class _BackupListTile extends StatelessWidget {
1102+
const _BackupListTile({
1103+
required this.backup,
1104+
required this.localeTag,
1105+
required this.restoring,
1106+
required this.onRestore,
1107+
});
1108+
1109+
final WebDavBackupEntry backup;
1110+
final String localeTag;
1111+
final bool restoring;
1112+
final VoidCallback onRestore;
1113+
1114+
@override
1115+
Widget build(BuildContext context) {
1116+
final modifiedAt = backup.modifiedAt;
1117+
final dateStr = modifiedAt != null
1118+
? DateFormat.yMd(localeTag).add_Hm().format(modifiedAt.toLocal())
1119+
: null;
1120+
final sizeStr = backup.sizeBytes != null
1121+
? _formatBytes(backup.sizeBytes!)
1122+
: null;
1123+
final subtitleParts = [?dateStr, ?sizeStr];
1124+
1125+
return ListTile(
1126+
contentPadding: EdgeInsets.zero,
1127+
leading: const Icon(Icons.cloud_outlined),
1128+
title: Text(backup.libraryName),
1129+
subtitle: subtitleParts.isNotEmpty
1130+
? Text(subtitleParts.join(' • '))
1131+
: null,
1132+
trailing: IconButton(
1133+
icon: restoring
1134+
? const SizedBox.square(
1135+
dimension: 16,
1136+
child: CircularProgressIndicator(strokeWidth: 2),
1137+
)
1138+
: const Icon(Icons.restore_outlined),
1139+
tooltip: 'restore_backup'.tr(),
1140+
onPressed: restoring ? null : onRestore,
1141+
),
1142+
);
1143+
}
1144+
1145+
String _formatBytes(int bytes) {
1146+
if (bytes >= 1024 * 1024) {
1147+
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
1148+
}
1149+
if (bytes >= 1024) {
1150+
return '${(bytes / 1024).toStringAsFixed(1)} KB';
1151+
}
1152+
return '$bytes B';
1153+
}
1154+
}

0 commit comments

Comments
 (0)