@@ -8,13 +8,13 @@ import 'package:package_info_plus/package_info_plus.dart';
88import '../../core/providers/app_providers.dart' ;
99import '../../core/security/security_preferences_service.dart' ;
1010import '../../core/session/app_session_controller.dart' ;
11+ import '../../core/storage/library_backup_service.dart' ;
1112import '../../core/update/app_update_controller.dart' ;
1213import '../../shared/utils/formatting.dart' ;
1314import '../../shared/widgets/app_updater.dart' ;
1415import '../../shared/widgets/app_error_state.dart' ;
1516import '../../shared/widgets/content_constraints.dart' ;
1617import '../setup/database_selection_sheet.dart' ;
17- import '../setup/webdav_restore_flow.dart' ;
1818import 'grade_system_controller.dart' ;
1919import 'grade_system_editor.dart' ;
2020import '../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