Files
b0esche_cloud/b0esche_cloud/lib/pages/file_explorer.dart

1205 lines
47 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:file_picker/file_picker.dart' hide FileType;
import 'package:path/path.dart' as p;
import 'dart:html' as html;
import '../blocs/file_browser/file_browser_bloc.dart';
import '../blocs/file_browser/file_browser_event.dart';
import '../blocs/file_browser/file_browser_state.dart';
import '../blocs/permission/permission_bloc.dart';
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';
import 'document_viewer.dart';
import 'editor_page.dart';
import '../injection.dart';
import '../services/file_service.dart';
class FileExplorer extends StatefulWidget {
final String orgId;
const FileExplorer({super.key, required this.orgId});
@override
State<FileExplorer> createState() => _FileExplorerState();
}
class _FileExplorerState extends State<FileExplorer> {
String? _selectedFilePath;
bool _isSearching = false;
bool _showField = false;
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
final Map<String, bool> _hovered = {};
String _getParentPath(String path) {
if (path == '/') return '/';
final parts = path.split('/').where((p) => p.isNotEmpty).toList();
if (parts.isEmpty) return '/';
parts.removeLast();
return '/${parts.join('/')}';
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: widget.orgId, path: '/'),
);
context.read<PermissionBloc>().add(LoadPermissions(widget.orgId));
}
@override
void didUpdateWidget(covariant FileExplorer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.orgId != widget.orgId) {
// Reset and reload when switching between Personal and Org workspaces
final bloc = context.read<FileBrowserBloc>();
bloc.add(ResetFileBrowser());
bloc.add(LoadDirectory(orgId: widget.orgId, path: '/'));
}
}
Future<String?> _showCreateFolderDialog(BuildContext context) async {
final TextEditingController controller = TextEditingController();
return showDialog<String>(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Container(
decoration: AppTheme.glassDecoration,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'New Folder',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: controller,
autofocus: true,
style: const TextStyle(color: AppTheme.primaryText),
cursorColor: AppTheme.accentColor,
decoration: InputDecoration(
hintText: 'Enter folder name',
hintStyle: const TextStyle(color: AppTheme.secondaryText),
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.accentColor.withValues(alpha: 0.5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.secondaryText.withValues(alpha: 0.5),
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
borderSide: BorderSide(color: AppTheme.accentColor),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Cancel',
style: TextStyle(color: AppTheme.primaryText),
),
),
TextButton(
onPressed: () {
final folderName = controller.text.trim();
if (folderName.isNotEmpty) {
// Strip any leading/trailing slashes so we only send the bare folder name
final sanitized = folderName
.replaceAll(RegExp(r'^/+'), '')
.replaceAll(RegExp(r'/+$'), '');
if (sanitized.isNotEmpty) {
Navigator.of(context).pop(sanitized);
}
}
},
child: const Text(
'Create',
style: TextStyle(
color: AppTheme.accentColor,
decoration: TextDecoration.underline,
decorationColor: AppTheme.accentColor,
decorationThickness: 1.5,
),
),
),
],
),
],
),
),
),
);
},
);
}
void _editFile(FileItem file) {
_showRenameDialog(file);
}
Future<void> _showRenameDialog(FileItem file) async {
final TextEditingController controller = TextEditingController(
text: file.name,
);
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Container(
decoration: AppTheme.glassDecoration,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Rename ${file.type == FileType.folder ? 'Folder' : 'File'}',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextField(
controller: controller,
autofocus: true,
style: const TextStyle(color: AppTheme.primaryText),
cursorColor: AppTheme.accentColor,
decoration: InputDecoration(
hintText:
'Enter new ${file.type == FileType.folder ? 'folder' : 'file'} name',
hintStyle: const TextStyle(color: AppTheme.secondaryText),
contentPadding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.accentColor.withValues(alpha: 0.5),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.secondaryText.withValues(alpha: 0.5),
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
borderSide: BorderSide(color: AppTheme.accentColor),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Cancel',
style: TextStyle(color: AppTheme.primaryText),
),
),
TextButton(
onPressed: () {
final newName = controller.text.trim();
if (newName.isNotEmpty && newName != file.name) {
context.read<FileBrowserBloc>().add(
RenameFile(
orgId: widget.orgId,
path: file.path,
newName: newName,
),
);
Navigator.of(context).pop();
}
},
child: const Text(
'Rename',
style: TextStyle(
color: AppTheme.accentColor,
decoration: TextDecoration.underline,
decorationColor: AppTheme.accentColor,
decorationThickness: 1.5,
),
),
),
],
),
],
),
),
),
);
},
);
}
void _downloadFile(FileItem file) async {
try {
final fileService = getIt<FileService>();
final downloadUrl = await fileService.getDownloadUrl(
widget.orgId,
file.path,
);
// For web, use the download URL with the ApiClient base URL (from DI)
final fullUrl = '${fileService.baseUrl}$downloadUrl';
// Trigger download via anchor element
html.AnchorElement(href: fullUrl)
..setAttribute('download', file.name)
..click();
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Downloading ${file.name}')));
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Download failed: $e')));
}
}
}
void _sendFile(FileItem file) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Send ${file.name}')));
}
Future<void> _deleteFile(FileItem file) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: Container(
decoration: AppTheme.glassDecoration,
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Delete ${file.name}?',
style: TextStyle(
color: AppTheme.primaryText,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'This action cannot be undone.',
style: TextStyle(color: AppTheme.primaryText),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text(
'Cancel',
style: TextStyle(color: AppTheme.primaryText),
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text(
'Delete',
style: TextStyle(
color: Colors.red,
decoration: TextDecoration.underline,
decorationColor: Colors.red,
decorationThickness: 1.5,
),
),
),
],
),
],
),
),
),
);
},
);
if (confirmed == true) {
context.read<FileBrowserBloc>().add(
DeleteFile(orgId: widget.orgId, path: file.path),
);
}
}
Widget _buildTitle() {
const double titleWidth = 72.0;
return SizedBox(
width: double.infinity,
height: 50,
child: Stack(
alignment: Alignment.centerLeft,
children: [
const Positioned(
left: 0,
child: Text(
'/Drive',
style: TextStyle(
fontSize: 24,
color: AppTheme.primaryText,
fontWeight: FontWeight.bold,
),
),
),
AnimatedPositioned(
left: _isSearching ? titleWidth + 250.0 : titleWidth,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: IconButton(
icon: Icon(
_isSearching ? Icons.close : Icons.search,
color: AppTheme.accentColor,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () {
if (_isSearching) {
setState(() {
_showField = false;
_isSearching = false;
_searchController.clear();
_searchQuery = '';
context.read<FileBrowserBloc>().add(ApplyFilter(''));
});
} else {
setState(() {
_isSearching = true;
});
Future.delayed(const Duration(milliseconds: 150), () {
setState(() {
_showField = true;
});
});
}
},
),
),
),
Positioned(
right: 0,
top: 0,
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: BlocBuilder<FileBrowserBloc, FileBrowserState>(
builder: (context, state) {
String currentSort = 'name';
bool isAscending = true;
if (state is DirectoryLoaded) {
currentSort = state.sortBy;
isAscending = state.isAscending;
}
return Row(
children: [
PopupMenuButton<String>(
color: AppTheme.accentColor.withAlpha(220),
position: PopupMenuPosition.under,
offset: const Offset(48, 8),
itemBuilder: (BuildContext context) => const [
PopupMenuItem(value: 'name', child: Text('Name')),
PopupMenuItem(value: 'date', child: Text('Date')),
PopupMenuItem(value: 'size', child: Text('Size')),
PopupMenuItem(value: 'type', child: Text('Type')),
],
onSelected: (value) {
context.read<FileBrowserBloc>().add(
ApplySort(value, isAscending: isAscending),
);
},
child: Row(
children: [
Icon(
Icons.arrow_drop_down,
color: AppTheme.primaryText,
),
const SizedBox(width: 4),
Text(
currentSort == 'name'
? 'Name'
: currentSort == 'date'
? 'Date'
: currentSort == 'size'
? 'Size'
: 'Type',
style: const TextStyle(
color: AppTheme.primaryText,
fontSize: 14,
),
),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: Icon(
isAscending
? Icons.arrow_upward
: Icons.arrow_downward,
color: AppTheme.accentColor,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () {
context.read<FileBrowserBloc>().add(
ApplySort(currentSort, isAscending: !isAscending),
);
},
),
],
);
},
),
),
),
if (_showField)
AnimatedPositioned(
left: titleWidth,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: SizedBox(
width: 250,
child: TextField(
controller: _searchController,
autofocus: true,
style: const TextStyle(color: AppTheme.primaryText),
cursorColor: AppTheme.accentColor,
decoration: InputDecoration(
hintText: 'Search Files...',
hintStyle: const TextStyle(color: AppTheme.secondaryText),
contentPadding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide(
color: AppTheme.secondaryText.withValues(alpha: 0.5),
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(24)),
borderSide: BorderSide(color: AppTheme.accentColor),
),
),
onChanged: (value) {
_searchQuery = value;
context.read<FileBrowserBloc>().add(
ApplyFilter(_searchQuery),
);
},
),
),
),
],
),
);
}
Widget _buildFileItem(
FileItem file,
bool isSelected,
bool isHovered,
bool isDraggedOver,
) {
return MouseRegion(
onEnter: (_) => setState(() => _hovered[file.path] = true),
onExit: (_) => setState(() => _hovered[file.path] = false),
child: GestureDetector(
onTap: () {
setState(() {
_selectedFilePath = file.path;
});
if (file.type == FileType.folder) {
context.read<FileBrowserBloc>().add(NavigateToFolder(file.path));
} else {
if (file.id == null || file.id!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error: File ID is missing')),
);
return;
}
_showDocumentViewer(widget.orgId, file.id!);
}
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: isSelected
? Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.4),
width: 2,
)
: isDraggedOver
? Border.all(color: Colors.blue, width: 2)
: null,
color: isSelected
? AppTheme.accentColor.withValues(alpha: 0.08)
: isHovered
? Colors.white.withValues(alpha: 0.05)
: isDraggedOver
? Colors.blue.withValues(alpha: 0.1)
: Colors.transparent,
),
child: ListTile(
leading: file.type == FileType.folder
? Icon(Icons.folder, color: AppTheme.primaryText, size: 36)
: Stack(
alignment: Alignment.bottomRight,
children: [
Icon(
Icons.insert_drive_file,
color: AppTheme.primaryText,
size: 36,
),
Padding(
padding: const EdgeInsets.only(bottom: 2, right: 6),
child: Text(
p.extension(file.name).toUpperCase(),
style: const TextStyle(
color: Colors.black,
fontSize: 6.25,
fontWeight: FontWeight.bold,
),
),
),
],
),
title: Text(
file.type == FileType.folder
? (file.name.startsWith('/') ? file.name : '/${file.name}')
: file.name,
style: const TextStyle(color: AppTheme.primaryText),
),
subtitle: Text(
file.type == FileType.folder
? 'Folder'
: 'File - ${file.size} bytes',
style: const TextStyle(color: AppTheme.secondaryText),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: AppTheme.secondaryText),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => _editFile(file),
),
IconButton(
icon: const Icon(
Icons.download,
color: AppTheme.secondaryText,
),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => _downloadFile(file),
),
IconButton(
icon: const Icon(Icons.send, color: AppTheme.secondaryText),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => _sendFile(file),
),
IconButton(
icon: const Icon(Icons.delete, color: AppTheme.secondaryText),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => _deleteFile(file),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return BlocListener<UploadBloc, UploadState>(
listener: (context, uploadState) {
if (uploadState is UploadInProgress) {
// Show error if any upload failed
for (final upload in uploadState.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) {
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 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(),
),
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: '/',
folderName: folderName,
),
);
}
},
child: const Row(
children: [
Icon(Icons.create_new_folder),
SizedBox(width: 8),
Text('New Folder'),
],
),
),
],
);
}
return const SizedBox.shrink();
},
),
// Only show back button and "Empty Folder" text if not at root
if (state.currentPath != '/')
Column(
children: [
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(),
),
),
),
],
),
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();
},
),
);
}
void _showDocumentViewer(String orgId, String fileId) {
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(16),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: MediaQuery.of(context).size.height * 0.9,
),
decoration: BoxDecoration(
color: AppTheme.primaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: DocumentViewerModal(
orgId: orgId,
fileId: fileId,
onEdit: () {
Navigator.of(context).pop();
_showDocumentEditor(orgId, fileId);
},
onClose: () => Navigator.of(context).pop(),
),
),
),
),
);
},
);
}
void _showDocumentEditor(String orgId, String fileId) {
showDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.transparent,
builder: (BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(16),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
maxHeight: MediaQuery.of(context).size.height * 0.9,
),
decoration: BoxDecoration(
color: AppTheme.primaryBackground,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.accentColor.withValues(alpha: 0.3),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: EditorPageModal(
orgId: orgId,
fileId: fileId,
onClose: () => Navigator.of(context).pop(),
),
),
),
),
);
},
);
}
}