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/upload/upload_bloc.dart';
import '../blocs/upload/upload_event.dart';
import '../blocs/upload/upload_state.dart';
import '../models/file_item.dart';
import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart';
@@ -661,393 +662,416 @@ class _FileExplorerState extends State<FileExplorer> {
@override
Widget build(BuildContext context) {
return BlocBuilder<FileBrowserBloc, FileBrowserState>(
builder: (context, state) {
if (state is DirectoryLoading) {
return Center(
child: CircularProgressIndicator(color: AppTheme.accentColor),
);
return BlocListener<UploadBloc, UploadState>(
listener: (context, uploadState) {
if (uploadState is UploadInProgress) {
final hasCompleted = uploadState.uploads.any((u) => u.isCompleted);
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) {
return Center(
child: Text(
'Error: ${state.error}',
style: const TextStyle(color: AppTheme.primaryText),
),
);
}
if (state is DirectoryError) {
return Center(
child: Text(
'Error: ${state.error}',
style: const TextStyle(color: AppTheme.primaryText),
),
);
}
if (state is DirectoryEmpty) {
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,
if (state is DirectoryEmpty) {
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(),
),
child: _buildTitle(),
),
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();
if (result != null && result.files.isNotEmpty) {
final files = result.files
.map(
(file) => FileItem(
name: file.name,
path: '/${file.name}',
type: FileType.file,
size: file.size,
lastModified: DateTime.now(),
localPath: file.path,
bytes: file.bytes,
),
)
.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: '/',
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,
),
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,
),
);
}).toList(),
}
},
child: const Row(
children: [
Icon(Icons.upload),
SizedBox(width: 8),
Text('Upload File'),
],
),
),
),
],
),
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();
if (result != null && result.files.isNotEmpty) {
final files = result.files
.map(
(file) => FileItem(
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,
),
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'),
],
),
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),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(
Icons.chevron_left,
Icons.arrow_back,
color: AppTheme.primaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: state.currentPage > 1
? () {
context.read<FileBrowserBloc>().add(
LoadPage(state.currentPage - 1),
);
}
: null,
onPressed: () {
final parentPath = _getParentPath(state.currentPath);
context.read<FileBrowserBloc>().add(
LoadDirectory(
orgId: widget.orgId,
path: parentPath,
),
);
},
),
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,
const Text(
'Empty Folder',
style: TextStyle(color: AppTheme.primaryText),
),
],
),
],
],
),
);
}
),
);
}
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) {
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 =
state.selectedOrg?.id ?? '';
// Reload file browser when org changes (or when falling back to personal workspace)