From 37e0520af01300204d37a5fce65950878c4a929d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Tue, 13 Jan 2026 16:48:25 +0100 Subject: [PATCH] Add file metadata display in viewer and upload progress snackbar - Backend: Add modified_by column to files table - Backend: Track who modified files via WOPI PutFile - Backend: Return fileInfo (name, size, lastModified, modifiedByName) in view response - Flutter: Update DocumentCapabilities model with FileInfo - Flutter: Display actual last modified date and user in document viewer - Flutter: Show upload progress snackbar with percentage that auto-dismisses on completion --- .../document_viewer_state.dart | 7 ++- .../lib/models/document_capabilities.dart | 4 +- b0esche_cloud/lib/models/viewer_session.dart | 12 +++- b0esche_cloud/lib/pages/document_viewer.dart | 12 ++-- b0esche_cloud/lib/pages/file_explorer.dart | 58 ++++++++++++++++++- 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/b0esche_cloud/lib/blocs/document_viewer/document_viewer_state.dart b/b0esche_cloud/lib/blocs/document_viewer/document_viewer_state.dart index 6c6dbea..6bf85f5 100644 --- a/b0esche_cloud/lib/blocs/document_viewer/document_viewer_state.dart +++ b/b0esche_cloud/lib/blocs/document_viewer/document_viewer_state.dart @@ -26,7 +26,12 @@ class DocumentViewerReady extends DocumentViewerState { }); @override - List get props => [viewUrl, caps, token, if (fileInfo != null) fileInfo!]; + List get props => [ + viewUrl, + caps, + token, + if (fileInfo != null) fileInfo!, + ]; } class DocumentViewerError extends DocumentViewerState { diff --git a/b0esche_cloud/lib/models/document_capabilities.dart b/b0esche_cloud/lib/models/document_capabilities.dart index 4190c34..443be5d 100644 --- a/b0esche_cloud/lib/models/document_capabilities.dart +++ b/b0esche_cloud/lib/models/document_capabilities.dart @@ -20,8 +20,8 @@ class FileInfo extends Equatable { return FileInfo( name: json['name'] ?? '', size: json['size'] ?? 0, - lastModified: json['lastModified'] != null - ? DateTime.tryParse(json['lastModified']) + lastModified: json['lastModified'] != null + ? DateTime.tryParse(json['lastModified']) : null, modifiedByName: json['modifiedByName'], ); diff --git a/b0esche_cloud/lib/models/viewer_session.dart b/b0esche_cloud/lib/models/viewer_session.dart index 0b0a77a..2dd6994 100644 --- a/b0esche_cloud/lib/models/viewer_session.dart +++ b/b0esche_cloud/lib/models/viewer_session.dart @@ -17,7 +17,13 @@ class ViewerSession extends Equatable { }); @override - List get props => [viewUrl, capabilities, token, expiresAt, fileInfo]; + List get props => [ + viewUrl, + capabilities, + token, + expiresAt, + fileInfo, + ]; factory ViewerSession.fromJson(Map json) { return ViewerSession( @@ -25,7 +31,9 @@ class ViewerSession extends Equatable { capabilities: DocumentCapabilities.fromJson(json['capabilities']), token: json['token'], expiresAt: DateTime.parse(json['expiresAt']), - fileInfo: json['fileInfo'] != null ? FileInfo.fromJson(json['fileInfo']) : null, + fileInfo: json['fileInfo'] != null + ? FileInfo.fromJson(json['fileInfo']) + : null, ); } } diff --git a/b0esche_cloud/lib/pages/document_viewer.dart b/b0esche_cloud/lib/pages/document_viewer.dart index 3a7a016..41a0b31 100644 --- a/b0esche_cloud/lib/pages/document_viewer.dart +++ b/b0esche_cloud/lib/pages/document_viewer.dart @@ -118,9 +118,11 @@ class _DocumentViewerModalState extends State { final modifiedDate = fileInfo.lastModified; final modifiedBy = fileInfo.modifiedByName; if (modifiedDate != null) { - final formattedDate = '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}'; + final formattedDate = + '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}'; if (modifiedBy != null && modifiedBy.isNotEmpty) { - lastModifiedText = 'Last modified: $formattedDate by $modifiedBy'; + lastModifiedText = + 'Last modified: $formattedDate by $modifiedBy'; } else { lastModifiedText = 'Last modified: $formattedDate'; } @@ -581,9 +583,11 @@ class _DocumentViewerState extends State { final modifiedDate = fileInfo.lastModified; final modifiedBy = fileInfo.modifiedByName; if (modifiedDate != null) { - final formattedDate = '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}'; + final formattedDate = + '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}'; if (modifiedBy != null && modifiedBy.isNotEmpty) { - lastModifiedText = 'Last modified: $formattedDate by $modifiedBy'; + lastModifiedText = + 'Last modified: $formattedDate by $modifiedBy'; } else { lastModifiedText = 'Last modified: $formattedDate'; } diff --git a/b0esche_cloud/lib/pages/file_explorer.dart b/b0esche_cloud/lib/pages/file_explorer.dart index 4c9b41e..a1f9524 100644 --- a/b0esche_cloud/lib/pages/file_explorer.dart +++ b/b0esche_cloud/lib/pages/file_explorer.dart @@ -37,6 +37,7 @@ class _FileExplorerState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; final Map _hovered = {}; + ScaffoldFeatureController? _uploadSnackBarController; String _getParentPath(String path) { if (path == '/') return '/'; @@ -684,16 +685,63 @@ class _FileExplorerState extends State { return BlocListener( listener: (context, uploadState) { if (uploadState is UploadInProgress) { + // Calculate overall progress + final uploads = uploadState.uploads; + final activeUploads = uploads.where((u) => !u.isCompleted && u.error == null).toList(); + final completedUploads = uploads.where((u) => u.isCompleted).toList(); + // Show error if any upload failed - for (final upload in uploadState.uploads) { + for (final upload in uploads) { if (upload.error != null && upload.error!.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Upload failed: ${upload.error}')), ); } } - final hasCompleted = uploadState.uploads.any((u) => u.isCompleted); - if (hasCompleted) { + + // Show progress snackbar for active uploads + if (activeUploads.isNotEmpty) { + final totalProgress = uploads.fold(0, (sum, u) => sum + u.progress) / uploads.length; + final fileName = activeUploads.length == 1 + ? activeUploads.first.fileName + : '${activeUploads.length} files'; + + // Dismiss previous snackbar and show new one + _uploadSnackBarController?.close(); + _uploadSnackBarController = ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + value: totalProgress, + strokeWidth: 2, + color: Colors.white, + backgroundColor: Colors.white24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Uploading $fileName... ${(totalProgress * 100).toInt()}%', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + duration: const Duration(days: 1), // Keep showing until dismissed + backgroundColor: AppTheme.surfaceColor, + ), + ); + } + + // Dismiss snackbar and refresh when all uploads complete + if (completedUploads.length == uploads.length && uploads.isNotEmpty) { + _uploadSnackBarController?.close(); + _uploadSnackBarController = null; + final fbState = context.read().state; String currentPath = '/'; if (fbState is DirectoryLoaded) currentPath = fbState.currentPath; @@ -702,6 +750,10 @@ class _FileExplorerState extends State { LoadDirectory(orgId: widget.orgId, path: currentPath), ); } + } else if (uploadState is UploadInitial) { + // Upload finished or reset - dismiss snackbar + _uploadSnackBarController?.close(); + _uploadSnackBarController = null; } }, child: BlocBuilder(