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
This commit is contained in:
@@ -26,7 +26,12 @@ class DocumentViewerReady extends DocumentViewerState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [viewUrl, caps, token, if (fileInfo != null) fileInfo!];
|
List<Object> get props => [
|
||||||
|
viewUrl,
|
||||||
|
caps,
|
||||||
|
token,
|
||||||
|
if (fileInfo != null) fileInfo!,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentViewerError extends DocumentViewerState {
|
class DocumentViewerError extends DocumentViewerState {
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ class ViewerSession extends Equatable {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [viewUrl, capabilities, token, expiresAt, fileInfo];
|
List<Object?> get props => [
|
||||||
|
viewUrl,
|
||||||
|
capabilities,
|
||||||
|
token,
|
||||||
|
expiresAt,
|
||||||
|
fileInfo,
|
||||||
|
];
|
||||||
|
|
||||||
factory ViewerSession.fromJson(Map<String, dynamic> json) {
|
factory ViewerSession.fromJson(Map<String, dynamic> json) {
|
||||||
return ViewerSession(
|
return ViewerSession(
|
||||||
@@ -25,7 +31,9 @@ class ViewerSession extends Equatable {
|
|||||||
capabilities: DocumentCapabilities.fromJson(json['capabilities']),
|
capabilities: DocumentCapabilities.fromJson(json['capabilities']),
|
||||||
token: json['token'],
|
token: json['token'],
|
||||||
expiresAt: DateTime.parse(json['expiresAt']),
|
expiresAt: DateTime.parse(json['expiresAt']),
|
||||||
fileInfo: json['fileInfo'] != null ? FileInfo.fromJson(json['fileInfo']) : null,
|
fileInfo: json['fileInfo'] != null
|
||||||
|
? FileInfo.fromJson(json['fileInfo'])
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,11 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
final modifiedDate = fileInfo.lastModified;
|
final modifiedDate = fileInfo.lastModified;
|
||||||
final modifiedBy = fileInfo.modifiedByName;
|
final modifiedBy = fileInfo.modifiedByName;
|
||||||
if (modifiedDate != null) {
|
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) {
|
if (modifiedBy != null && modifiedBy.isNotEmpty) {
|
||||||
lastModifiedText = 'Last modified: $formattedDate by $modifiedBy';
|
lastModifiedText =
|
||||||
|
'Last modified: $formattedDate by $modifiedBy';
|
||||||
} else {
|
} else {
|
||||||
lastModifiedText = 'Last modified: $formattedDate';
|
lastModifiedText = 'Last modified: $formattedDate';
|
||||||
}
|
}
|
||||||
@@ -581,9 +583,11 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
final modifiedDate = fileInfo.lastModified;
|
final modifiedDate = fileInfo.lastModified;
|
||||||
final modifiedBy = fileInfo.modifiedByName;
|
final modifiedBy = fileInfo.modifiedByName;
|
||||||
if (modifiedDate != null) {
|
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) {
|
if (modifiedBy != null && modifiedBy.isNotEmpty) {
|
||||||
lastModifiedText = 'Last modified: $formattedDate by $modifiedBy';
|
lastModifiedText =
|
||||||
|
'Last modified: $formattedDate by $modifiedBy';
|
||||||
} else {
|
} else {
|
||||||
lastModifiedText = 'Last modified: $formattedDate';
|
lastModifiedText = 'Last modified: $formattedDate';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
final Map<String, bool> _hovered = {};
|
final Map<String, bool> _hovered = {};
|
||||||
|
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? _uploadSnackBarController;
|
||||||
|
|
||||||
String _getParentPath(String path) {
|
String _getParentPath(String path) {
|
||||||
if (path == '/') return '/';
|
if (path == '/') return '/';
|
||||||
@@ -684,16 +685,63 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
return BlocListener<UploadBloc, UploadState>(
|
return BlocListener<UploadBloc, UploadState>(
|
||||||
listener: (context, uploadState) {
|
listener: (context, uploadState) {
|
||||||
if (uploadState is UploadInProgress) {
|
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
|
// Show error if any upload failed
|
||||||
for (final upload in uploadState.uploads) {
|
for (final upload in uploads) {
|
||||||
if (upload.error != null && upload.error!.isNotEmpty) {
|
if (upload.error != null && upload.error!.isNotEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Upload failed: ${upload.error}')),
|
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<double>(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<FileBrowserBloc>().state;
|
final fbState = context.read<FileBrowserBloc>().state;
|
||||||
String currentPath = '/';
|
String currentPath = '/';
|
||||||
if (fbState is DirectoryLoaded) currentPath = fbState.currentPath;
|
if (fbState is DirectoryLoaded) currentPath = fbState.currentPath;
|
||||||
@@ -702,6 +750,10 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
LoadDirectory(orgId: widget.orgId, path: currentPath),
|
LoadDirectory(orgId: widget.orgId, path: currentPath),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (uploadState is UploadInitial) {
|
||||||
|
// Upload finished or reset - dismiss snackbar
|
||||||
|
_uploadSnackBarController?.close();
|
||||||
|
_uploadSnackBarController = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
|
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
|
||||||
|
|||||||
Reference in New Issue
Block a user