move files per drag and drop
This commit is contained in:
@@ -14,6 +14,7 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
int _pageSize = 20;
|
||||
String _sortBy = 'name';
|
||||
bool _isAscending = true;
|
||||
String _currentFilter = '';
|
||||
|
||||
FileBrowserBloc(this._fileService) : super(DirectoryInitial()) {
|
||||
on<LoadDirectory>(_onLoadDirectory);
|
||||
@@ -22,6 +23,7 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
on<ApplySort>(_onApplySort);
|
||||
on<ApplyFilter>(_onApplyFilter);
|
||||
on<CreateFolder>(_onCreateFolder);
|
||||
on<MoveFile>(_onMoveFile);
|
||||
on<ResetFileBrowser>(_onResetFileBrowser);
|
||||
on<LoadPage>(_onLoadPage);
|
||||
on<ChangePageSize>(_onChangePageSize);
|
||||
@@ -39,6 +41,7 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
final files = await _fileService.getFiles(event.orgId, event.path);
|
||||
_currentFiles = _sortFiles(files, _sortBy, _isAscending);
|
||||
_filteredFiles = _currentFiles;
|
||||
_currentFilter = '';
|
||||
_currentPage = 1;
|
||||
if (files.isEmpty) {
|
||||
emit(DirectoryEmpty(_currentPath));
|
||||
@@ -70,15 +73,16 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
_isAscending = event.isAscending;
|
||||
_currentFiles = _sortFiles(_currentFiles, _sortBy, _isAscending);
|
||||
_filteredFiles = _currentFiles
|
||||
.where((f) => f.name.toLowerCase().contains(''))
|
||||
.toList(); // Re-apply filter if any, but since filter is separate, perhaps need to track filter
|
||||
.where((f) => f.name.toLowerCase().contains(_currentFilter))
|
||||
.toList();
|
||||
_currentPage = 1;
|
||||
_emitLoadedState(emit);
|
||||
}
|
||||
|
||||
void _onApplyFilter(ApplyFilter event, Emitter<FileBrowserState> emit) {
|
||||
_currentFilter = event.filter.toLowerCase();
|
||||
_filteredFiles = _currentFiles
|
||||
.where((f) => f.name.toLowerCase().contains(event.filter.toLowerCase()))
|
||||
.where((f) => f.name.toLowerCase().contains(_currentFilter))
|
||||
.toList();
|
||||
_currentPage = 1;
|
||||
_emitLoadedState(emit);
|
||||
@@ -94,12 +98,49 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
event.parentPath,
|
||||
event.folderName,
|
||||
);
|
||||
// Refresh the directory to show the new folder
|
||||
add(LoadDirectory(orgId: event.orgId, path: event.parentPath));
|
||||
// Add the new folder to local state if in current directory
|
||||
if (event.parentPath == _currentPath) {
|
||||
final newFolder = FileItem(
|
||||
name: event.folderName,
|
||||
path: '${event.parentPath}/${event.folderName}',
|
||||
type: FileType.folder,
|
||||
size: 0,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
_currentFiles.add(newFolder);
|
||||
_currentFiles = _sortFiles(_currentFiles, _sortBy, _isAscending);
|
||||
_filteredFiles = _currentFiles
|
||||
.where((f) => f.name.toLowerCase().contains(_currentFilter))
|
||||
.toList();
|
||||
_emitLoadedState(emit);
|
||||
} else {
|
||||
add(LoadDirectory(orgId: event.orgId, path: event.parentPath));
|
||||
}
|
||||
} catch (e) {
|
||||
// For now, emit error state or handle appropriately
|
||||
// Since states don't have error for create, perhaps refresh anyway
|
||||
add(LoadDirectory(orgId: event.orgId, path: event.parentPath));
|
||||
emit(DirectoryError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onMoveFile(MoveFile event, Emitter<FileBrowserState> emit) async {
|
||||
try {
|
||||
await _fileService.moveFile(
|
||||
event.orgId,
|
||||
event.sourcePath,
|
||||
event.targetPath,
|
||||
);
|
||||
// If moving into current directory, reload to get the new file details
|
||||
if (event.targetPath == _currentPath) {
|
||||
add(LoadDirectory(orgId: event.orgId, path: _currentPath));
|
||||
} else {
|
||||
// Remove the moved file if it was in current directory
|
||||
_currentFiles.removeWhere((f) => f.path == event.sourcePath);
|
||||
_filteredFiles = _currentFiles
|
||||
.where((f) => f.name.toLowerCase().contains(_currentFilter))
|
||||
.toList();
|
||||
_emitLoadedState(emit);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(DirectoryError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +151,7 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
emit(DirectoryInitial());
|
||||
_currentOrgId = '';
|
||||
_currentPath = '/';
|
||||
_currentFilter = '';
|
||||
_currentPage = 1;
|
||||
_pageSize = 20;
|
||||
_sortBy = 'name';
|
||||
@@ -133,6 +175,7 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||
final files = await _fileService.searchFiles(event.orgId, event.query);
|
||||
_currentFiles = files;
|
||||
_filteredFiles = files;
|
||||
_currentFilter = '';
|
||||
_currentPage = 1;
|
||||
if (files.isEmpty) {
|
||||
emit(DirectoryEmpty(_currentPath));
|
||||
|
||||
@@ -91,3 +91,18 @@ class SearchFiles extends FileBrowserEvent {
|
||||
@override
|
||||
List<Object> get props => [orgId, query];
|
||||
}
|
||||
|
||||
class MoveFile extends FileBrowserEvent {
|
||||
final String orgId;
|
||||
final String sourcePath;
|
||||
final String targetPath;
|
||||
|
||||
const MoveFile({
|
||||
required this.orgId,
|
||||
required this.sourcePath,
|
||||
required this.targetPath,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [orgId, sourcePath, targetPath];
|
||||
}
|
||||
|
||||
@@ -93,11 +93,9 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
color: AppTheme.secondaryText.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
borderSide: BorderSide(color: AppTheme.accentColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -144,21 +142,18 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
}
|
||||
|
||||
void _editFile(FileItem file) {
|
||||
// Placeholder for edit action
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Edit ${file.name}')));
|
||||
}
|
||||
|
||||
void _downloadFile(FileItem file) {
|
||||
// Placeholder for download action
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Download ${file.name}')));
|
||||
}
|
||||
|
||||
void _sendFile(FileItem file) {
|
||||
// Placeholder for send action
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Send ${file.name}')));
|
||||
@@ -322,9 +317,9 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
color: AppTheme.secondaryText.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: const BorderSide(color: AppTheme.accentColor),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(24)),
|
||||
borderSide: BorderSide(color: AppTheme.accentColor),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
@@ -341,6 +336,97 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
context.go('/viewer/${file.name}');
|
||||
}
|
||||
},
|
||||
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: Icon(
|
||||
file.type == FileType.folder
|
||||
? Icons.folder
|
||||
: Icons.insert_drive_file,
|
||||
color: AppTheme.primaryText,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<FileBrowserBloc, FileBrowserState>(
|
||||
@@ -350,6 +436,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
child: CircularProgressIndicator(color: AppTheme.accentColor),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DirectoryError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
@@ -358,6 +445,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DirectoryEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -425,7 +513,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
context.read<FileBrowserBloc>().add(
|
||||
CreateFolder(
|
||||
orgId: 'org1',
|
||||
parentPath: state.currentPath,
|
||||
parentPath: '/',
|
||||
folderName: folderName,
|
||||
),
|
||||
);
|
||||
@@ -456,21 +544,9 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: () {
|
||||
final parentPath = state.currentPath == '/'
|
||||
? '/'
|
||||
: state.currentPath
|
||||
.substring(
|
||||
0,
|
||||
state.currentPath.lastIndexOf('/'),
|
||||
)
|
||||
.isEmpty
|
||||
? '/'
|
||||
: state.currentPath.substring(
|
||||
0,
|
||||
state.currentPath.lastIndexOf('/'),
|
||||
);
|
||||
final currentPath = '/';
|
||||
context.read<FileBrowserBloc>().add(
|
||||
LoadDirectory(orgId: 'org1', path: parentPath),
|
||||
LoadDirectory(orgId: 'org1', path: currentPath),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -484,6 +560,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DirectoryLoaded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -502,7 +579,6 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
child: _buildTitle(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Breadcrumbs and back button
|
||||
Visibility(
|
||||
visible: state.breadcrumbs.isNotEmpty,
|
||||
child: Column(
|
||||
@@ -625,111 +701,61 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
final file = state.paginatedFiles[index];
|
||||
final isSelected = _selectedFilePath == file.path;
|
||||
final isHovered = _hovered[file.path] ?? false;
|
||||
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 {
|
||||
// Open viewer
|
||||
context.go('/viewer/${file.name}');
|
||||
}
|
||||
},
|
||||
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,
|
||||
)
|
||||
: null,
|
||||
color: isSelected
|
||||
? AppTheme.accentColor.withValues(alpha: 0.08)
|
||||
: isHovered
|
||||
? Colors.white.withValues(alpha: 0.05)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
file.type == FileType.folder
|
||||
? Icons.folder
|
||||
: Icons.insert_drive_file,
|
||||
color: AppTheme.primaryText,
|
||||
),
|
||||
title: Text(
|
||||
file.type == FileType.folder
|
||||
? (file.name.startsWith('/')
|
||||
? file.name
|
||||
: '/${file.name}')
|
||||
: file.name,
|
||||
style: const TextStyle(
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
file.type == FileType.folder
|
||||
? 'Folder'
|
||||
: 'File - ${file.size} bytes',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.secondaryText,
|
||||
),
|
||||
child: _buildFileItem(
|
||||
file,
|
||||
isSelected,
|
||||
isHovered,
|
||||
isDraggedOver,
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onAccept: (draggedFile) {
|
||||
context.read<FileBrowserBloc>().add(
|
||||
MoveFile(
|
||||
orgId: 'org1',
|
||||
sourcePath: draggedFile.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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
// Pagination controls
|
||||
if (state.totalPages > 1) ...[
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -779,6 +805,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,5 +6,6 @@ abstract class FileRepository {
|
||||
Future<void> uploadFile(String orgId, FileItem file);
|
||||
Future<void> deleteFile(String orgId, String path);
|
||||
Future<void> createFolder(String orgId, String parentPath, String folderName);
|
||||
Future<void> moveFile(String orgId, String sourcePath, String targetPath);
|
||||
Future<List<FileItem>> searchFiles(String orgId, String query);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,28 @@ class MockFileRepository implements FileRepository {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> moveFile(
|
||||
String orgId,
|
||||
String sourcePath,
|
||||
String targetPath,
|
||||
) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
final fileIndex = _files.indexWhere((f) => f.path == sourcePath);
|
||||
if (fileIndex != -1) {
|
||||
final file = _files[fileIndex];
|
||||
final newName = file.path.split('/').last;
|
||||
final newPath = targetPath == '/' ? '/$newName' : '$targetPath/$newName';
|
||||
_files[fileIndex] = FileItem(
|
||||
name: file.name,
|
||||
path: newPath,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
@@ -45,6 +45,17 @@ class FileService {
|
||||
await _fileRepository.createFolder(orgId, parentPath, folderName);
|
||||
}
|
||||
|
||||
Future<void> moveFile(
|
||||
String orgId,
|
||||
String sourcePath,
|
||||
String targetPath,
|
||||
) async {
|
||||
if (sourcePath.isEmpty || targetPath.isEmpty) {
|
||||
throw Exception('Paths cannot be empty');
|
||||
}
|
||||
await _fileRepository.moveFile(orgId, sourcePath, targetPath);
|
||||
}
|
||||
|
||||
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||
if (query.isEmpty) {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user