Add error handling for organization loading in HomePage

This commit is contained in:
Leon Bösche
2026-01-09 23:14:45 +01:00
parent aac6d2eb46
commit 708d4ca790
2 changed files with 391 additions and 356 deletions

View File

@@ -12,6 +12,7 @@ import '../blocs/permission/permission_event.dart';
import '../blocs/permission/permission_state.dart'; import '../blocs/permission/permission_state.dart';
import '../blocs/upload/upload_bloc.dart'; import '../blocs/upload/upload_bloc.dart';
import '../blocs/upload/upload_event.dart'; import '../blocs/upload/upload_event.dart';
import '../blocs/upload/upload_state.dart';
import '../models/file_item.dart'; import '../models/file_item.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart'; import '../theme/modern_glass_button.dart';
@@ -661,393 +662,416 @@ class _FileExplorerState extends State<FileExplorer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FileBrowserBloc, FileBrowserState>( return BlocListener<UploadBloc, UploadState>(
builder: (context, state) { listener: (context, uploadState) {
if (state is DirectoryLoading) { if (uploadState is UploadInProgress) {
return Center( final hasCompleted = uploadState.uploads.any((u) => u.isCompleted);
child: CircularProgressIndicator(color: AppTheme.accentColor), if (hasCompleted) {
); final fbState = context.read<FileBrowserBloc>().state;
String currentPath = '/';
if (fbState is DirectoryLoaded) currentPath = fbState.currentPath;
if (fbState is DirectoryEmpty) currentPath = fbState.currentPath;
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: widget.orgId, path: currentPath),
);
}
} }
},
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
builder: (context, state) {
if (state is DirectoryLoading) {
return Center(
child: CircularProgressIndicator(color: AppTheme.accentColor),
);
}
if (state is DirectoryError) { if (state is DirectoryError) {
return Center( return Center(
child: Text( child: Text(
'Error: ${state.error}', 'Error: ${state.error}',
style: const TextStyle(color: AppTheme.primaryText), style: const TextStyle(color: AppTheme.primaryText),
), ),
); );
} }
if (state is DirectoryEmpty) { if (state is DirectoryEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: AppTheme.primaryText.withValues(alpha: 0.5), color: AppTheme.primaryText.withValues(alpha: 0.5),
width: 1, width: 1,
),
), ),
), ),
child: _buildTitle(),
), ),
child: _buildTitle(), const SizedBox(height: 16),
), BlocBuilder<PermissionBloc, PermissionState>(
const SizedBox(height: 16), builder: (context, permState) {
BlocBuilder<PermissionBloc, PermissionState>( if (permState is PermissionLoaded &&
builder: (context, permState) { permState.capabilities.canWrite) {
if (permState is PermissionLoaded && return Row(
permState.capabilities.canWrite) { children: [
return Row( ModernGlassButton(
children: [ onPressed: () async {
ModernGlassButton( final result = await FilePicker.platform
onPressed: () async { .pickFiles(withData: true);
final result = await FilePicker.platform if (result != null && result.files.isNotEmpty) {
.pickFiles(); final files = result.files
if (result != null && result.files.isNotEmpty) { .map(
final files = result.files (file) => FileItem(
.map( name: file.name,
(file) => FileItem( // Parent path only; server uses filename from multipart
name: file.name, path: state.currentPath,
path: '/${file.name}', type: FileType.file,
type: FileType.file, size: file.size,
size: file.size, lastModified: DateTime.now(),
lastModified: DateTime.now(), localPath: file.path,
localPath: file.path, bytes: file.bytes,
bytes: file.bytes, ),
), )
) .toList();
.toList(); context.read<UploadBloc>().add(
context.read<UploadBloc>().add( StartUpload(
StartUpload( files: files,
files: files, targetPath: state.currentPath,
targetPath: '/', orgId: widget.orgId,
orgId: widget.orgId,
),
);
}
},
child: const Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Upload File'),
],
),
),
const SizedBox(width: 16),
ModernGlassButton(
onPressed: () async {
final folderName = await _showCreateFolderDialog(
context,
);
if (folderName != null && folderName.isNotEmpty) {
context.read<FileBrowserBloc>().add(
CreateFolder(
orgId: widget.orgId,
parentPath: '/',
folderName: folderName,
),
);
}
},
child: const Row(
children: [
Icon(Icons.create_new_folder),
SizedBox(width: 8),
Text('New Folder'),
],
),
),
],
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 16),
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
color: AppTheme.primaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () {
final parentPath = _getParentPath(state.currentPath);
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: widget.orgId, path: parentPath),
);
},
),
const Text(
'Empty Folder',
style: TextStyle(color: AppTheme.primaryText),
),
],
),
],
),
);
}
if (state is DirectoryLoaded) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppTheme.primaryText.withValues(alpha: 0.5),
width: 1,
),
),
),
child: _buildTitle(),
),
const SizedBox(height: 16),
Visibility(
visible: state.breadcrumbs.isNotEmpty,
child: Column(
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
color: AppTheme.primaryText,
),
onPressed: () {
final parentPath = _getParentPath(
state.currentPath,
);
context.read<FileBrowserBloc>().add(
LoadDirectory(
orgId: widget.orgId,
path: parentPath,
),
);
},
),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.breadcrumbs.map((breadcrumb) {
return TextButton(
onPressed: () {
context.read<FileBrowserBloc>().add(
NavigateToFolder(breadcrumb.path),
);
},
child: Text(
'/${breadcrumb.name}',
style: const TextStyle(
color: AppTheme.secondaryText,
),
), ),
); );
}).toList(), }
},
child: const Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Upload File'),
],
), ),
), ),
), const SizedBox(width: 16),
], ModernGlassButton(
), onPressed: () async {
const SizedBox(height: 16), final folderName =
], await _showCreateFolderDialog(context);
), if (folderName != null &&
), folderName.isNotEmpty) {
BlocBuilder<PermissionBloc, PermissionState>( context.read<FileBrowserBloc>().add(
builder: (context, permState) { CreateFolder(
if (permState is PermissionLoaded && orgId: widget.orgId,
permState.capabilities.canWrite) { parentPath: '/',
return Row( folderName: folderName,
children: [ ),
ModernGlassButton( );
onPressed: () async { }
final result = await FilePicker.platform },
.pickFiles(); child: const Row(
if (result != null && result.files.isNotEmpty) { children: [
final files = result.files Icon(Icons.create_new_folder),
.map( SizedBox(width: 8),
(file) => FileItem( Text('New Folder'),
name: file.name, ],
path: '/${file.name}',
type: FileType.file,
size: file.size,
lastModified: DateTime.now(),
),
)
.toList();
context.read<UploadBloc>().add(
StartUpload(
files: files,
targetPath: '/',
orgId: widget.orgId,
),
);
}
},
child: const Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Upload File'),
],
),
),
const SizedBox(width: 16),
ModernGlassButton(
onPressed: () async {
final folderName = await _showCreateFolderDialog(
context,
);
if (folderName != null && folderName.isNotEmpty) {
context.read<FileBrowserBloc>().add(
CreateFolder(
orgId: widget.orgId,
parentPath: state.currentPath,
folderName: folderName,
),
);
}
},
child: const Row(
children: [
Icon(Icons.create_new_folder),
SizedBox(width: 8),
Text('New Folder'),
],
),
),
],
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: state.paginatedFiles.length,
itemBuilder: (context, index) {
final file = state.paginatedFiles[index];
final isSelected = _selectedFilePath == file.path;
final isHovered = _hovered[file.path] ?? false;
if (file.type == FileType.folder) {
return DragTarget<FileItem>(
builder: (context, candidate, rejected) {
final isDraggedOver = candidate.isNotEmpty;
return Draggable<FileItem>(
data: file,
feedback: Opacity(
opacity: 0.8,
child: Icon(
Icons.folder,
color: AppTheme.primaryText,
size: 48,
),
), ),
child: _buildFileItem(
file,
isSelected,
isHovered,
isDraggedOver,
),
);
},
onAcceptWithDetails: (draggedFile) {
context.read<FileBrowserBloc>().add(
MoveFile(
orgId: widget.orgId,
sourcePath: draggedFile.data.path,
targetPath: file.path,
),
);
},
);
} else {
return Draggable<FileItem>(
data: file,
feedback: Opacity(
opacity: 0.8,
child: Icon(
Icons.insert_drive_file,
color: AppTheme.primaryText,
size: 48,
), ),
), ],
child: _buildFileItem(
file,
isSelected,
isHovered,
false,
),
); );
} }
return const SizedBox.shrink();
}, },
), ),
),
if (state.totalPages > 1) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.chevron_left, Icons.arrow_back,
color: AppTheme.primaryText, color: AppTheme.primaryText,
), ),
splashColor: Colors.transparent, splashColor: Colors.transparent,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
onPressed: state.currentPage > 1 onPressed: () {
? () { final parentPath = _getParentPath(state.currentPath);
context.read<FileBrowserBloc>().add( context.read<FileBrowserBloc>().add(
LoadPage(state.currentPage - 1), LoadDirectory(
); orgId: widget.orgId,
} path: parentPath,
: null, ),
);
},
), ),
Text( const Text(
'${state.currentPage} / ${state.totalPages}', 'Empty Folder',
style: const TextStyle( style: TextStyle(color: AppTheme.primaryText),
color: AppTheme.primaryText,
fontSize: 16,
),
),
IconButton(
icon: const Icon(
Icons.chevron_right,
color: AppTheme.primaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: state.currentPage < state.totalPages
? () {
context.read<FileBrowserBloc>().add(
LoadPage(state.currentPage + 1),
);
}
: null,
), ),
], ],
), ),
], ],
], ),
), );
); }
}
return const SizedBox.shrink(); if (state is DirectoryLoaded) {
}, return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: AppTheme.primaryText.withValues(alpha: 0.5),
width: 1,
),
),
),
child: _buildTitle(),
),
const SizedBox(height: 16),
Visibility(
visible: state.breadcrumbs.isNotEmpty,
child: Column(
children: [
Row(
children: [
IconButton(
icon: const Icon(
Icons.arrow_back,
color: AppTheme.primaryText,
),
onPressed: () {
final parentPath = _getParentPath(
state.currentPath,
);
context.read<FileBrowserBloc>().add(
LoadDirectory(
orgId: widget.orgId,
path: parentPath,
),
);
},
),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: state.breadcrumbs.map((breadcrumb) {
return TextButton(
onPressed: () {
context.read<FileBrowserBloc>().add(
NavigateToFolder(breadcrumb.path),
);
},
child: Text(
'/${breadcrumb.name}',
style: const TextStyle(
color: AppTheme.secondaryText,
),
),
);
}).toList(),
),
),
),
],
),
const SizedBox(height: 16),
],
),
),
BlocBuilder<PermissionBloc, PermissionState>(
builder: (context, permState) {
if (permState is PermissionLoaded &&
permState.capabilities.canWrite) {
return Row(
children: [
ModernGlassButton(
onPressed: () async {
final result = await FilePicker.platform
.pickFiles(withData: true);
if (result != null && result.files.isNotEmpty) {
final files = result.files
.map(
(file) => FileItem(
name: file.name,
// Parent path only; server uses filename from multipart
path: state.currentPath,
type: FileType.file,
size: file.size,
lastModified: DateTime.now(),
localPath: file.path,
bytes: file.bytes,
),
)
.toList();
context.read<UploadBloc>().add(
StartUpload(
files: files,
targetPath: state.currentPath,
orgId: widget.orgId,
),
);
}
},
child: const Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Upload File'),
],
),
),
const SizedBox(width: 16),
ModernGlassButton(
onPressed: () async {
final folderName =
await _showCreateFolderDialog(context);
if (folderName != null &&
folderName.isNotEmpty) {
context.read<FileBrowserBloc>().add(
CreateFolder(
orgId: widget.orgId,
parentPath: state.currentPath,
folderName: folderName,
),
);
}
},
child: const Row(
children: [
Icon(Icons.create_new_folder),
SizedBox(width: 8),
Text('New Folder'),
],
),
),
],
);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
itemCount: state.paginatedFiles.length,
itemBuilder: (context, index) {
final file = state.paginatedFiles[index];
final isSelected = _selectedFilePath == file.path;
final isHovered = _hovered[file.path] ?? false;
if (file.type == FileType.folder) {
return DragTarget<FileItem>(
builder: (context, candidate, rejected) {
final isDraggedOver = candidate.isNotEmpty;
return Draggable<FileItem>(
data: file,
feedback: Opacity(
opacity: 0.8,
child: Icon(
Icons.folder,
color: AppTheme.primaryText,
size: 48,
),
),
child: _buildFileItem(
file,
isSelected,
isHovered,
isDraggedOver,
),
);
},
onAcceptWithDetails: (draggedFile) {
context.read<FileBrowserBloc>().add(
MoveFile(
orgId: widget.orgId,
sourcePath: draggedFile.data.path,
targetPath: file.path,
),
);
},
);
} else {
return Draggable<FileItem>(
data: file,
feedback: Opacity(
opacity: 0.8,
child: Icon(
Icons.insert_drive_file,
color: AppTheme.primaryText,
size: 48,
),
),
child: _buildFileItem(
file,
isSelected,
isHovered,
false,
),
);
}
},
),
),
if (state.totalPages > 1) ...[
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(
Icons.chevron_left,
color: AppTheme.primaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: state.currentPage > 1
? () {
context.read<FileBrowserBloc>().add(
LoadPage(state.currentPage - 1),
);
}
: null,
),
Text(
'${state.currentPage} / ${state.totalPages}',
style: const TextStyle(
color: AppTheme.primaryText,
fontSize: 16,
),
),
IconButton(
icon: const Icon(
Icons.chevron_right,
color: AppTheme.primaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: state.currentPage < state.totalPages
? () {
context.read<FileBrowserBloc>().add(
LoadPage(state.currentPage + 1),
);
}
: null,
),
],
),
],
],
),
);
}
return const SizedBox.shrink();
},
),
); );
} }

View File

@@ -345,6 +345,17 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
>( >(
listener: (context, state) { listener: (context, state) {
if (state is OrganizationLoaded) { if (state is OrganizationLoaded) {
// Show errors if present
if (state.error != null &&
state.error!.isNotEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(state.error!),
),
);
}
final orgId = final orgId =
state.selectedOrg?.id ?? ''; state.selectedOrg?.id ?? '';
// Reload file browser when org changes (or when falling back to personal workspace) // Reload file browser when org changes (or when falling back to personal workspace)