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
|
||||
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 {
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
|
||||
@@ -17,7 +17,13 @@ class ViewerSession extends Equatable {
|
||||
});
|
||||
|
||||
@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) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +118,11 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
||||
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<DocumentViewer> {
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
final Map<String, bool> _hovered = {};
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? _uploadSnackBarController;
|
||||
|
||||
String _getParentPath(String path) {
|
||||
if (path == '/') return '/';
|
||||
@@ -684,16 +685,63 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
return BlocListener<UploadBloc, UploadState>(
|
||||
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<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;
|
||||
String currentPath = '/';
|
||||
if (fbState is DirectoryLoaded) currentPath = fbState.currentPath;
|
||||
@@ -702,6 +750,10 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
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>(
|
||||
|
||||
Reference in New Issue
Block a user