delete files and folders

This commit is contained in:
Leon Bösche
2025-12-17 01:49:45 +01:00
parent 6e5d36a1ce
commit d3b2c138ee
8 changed files with 284 additions and 10 deletions

View File

@@ -3,6 +3,7 @@ import 'file_browser_event.dart';
import 'file_browser_state.dart';
import '../../services/file_service.dart';
import '../../models/file_item.dart';
import 'package:path/path.dart' as p;
class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
final FileService _fileService;
@@ -24,6 +25,8 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
on<ApplyFilter>(_onApplyFilter);
on<CreateFolder>(_onCreateFolder);
on<MoveFile>(_onMoveFile);
on<RenameFile>(_onRenameFile);
on<DeleteFile>(_onDeleteFile);
on<ResetFileBrowser>(_onResetFileBrowser);
on<LoadPage>(_onLoadPage);
on<ChangePageSize>(_onChangePageSize);
@@ -144,6 +147,39 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
}
}
void _onRenameFile(RenameFile event, Emitter<FileBrowserState> emit) async {
try {
await _fileService.renameFile(event.orgId, event.path, event.newName);
final index = _currentFiles.indexWhere((f) => f.path == event.path);
if (index != -1) {
final oldItem = _currentFiles[index];
final newPath = '${p.dirname(event.path)}/${event.newName}';
final newItem = oldItem.copyWith(name: event.newName, path: newPath);
_currentFiles[index] = newItem;
_currentFiles = _sortFiles(_currentFiles, _sortBy, _isAscending);
_filteredFiles = _currentFiles
.where((f) => f.name.toLowerCase().contains(_currentFilter))
.toList();
_emitLoadedState(emit);
}
} catch (e) {
emit(DirectoryError(e.toString()));
}
}
void _onDeleteFile(DeleteFile event, Emitter<FileBrowserState> emit) async {
try {
await _fileService.deleteFile(event.orgId, event.path);
_currentFiles.removeWhere((f) => f.path == event.path);
_filteredFiles = _currentFiles
.where((f) => f.name.toLowerCase().contains(_currentFilter))
.toList();
_emitLoadedState(emit);
} catch (e) {
emit(DirectoryError(e.toString()));
}
}
void _onResetFileBrowser(
ResetFileBrowser event,
Emitter<FileBrowserState> emit,

View File

@@ -106,3 +106,28 @@ class MoveFile extends FileBrowserEvent {
@override
List<Object> get props => [orgId, sourcePath, targetPath];
}
class RenameFile extends FileBrowserEvent {
final String orgId;
final String path;
final String newName;
const RenameFile({
required this.orgId,
required this.path,
required this.newName,
});
@override
List<Object> get props => [orgId, path, newName];
}
class DeleteFile extends FileBrowserEvent {
final String orgId;
final String path;
const DeleteFile({required this.orgId, required this.path});
@override
List<Object> get props => [orgId, path];
}

View File

@@ -29,6 +29,14 @@ class _FileExplorerState extends State<FileExplorer> {
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();
@@ -142,9 +150,110 @@ class _FileExplorerState extends State<FileExplorer> {
}
void _editFile(FileItem file) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Edit ${file.name}')));
_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: 'org1',
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) {
@@ -159,6 +268,72 @@ class _FileExplorerState extends State<FileExplorer> {
).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: 'org1', path: file.path),
);
}
}
Widget _buildTitle() {
const double titleWidth = 72.0;
return SizedBox(
@@ -419,6 +594,12 @@ class _FileExplorerState extends State<FileExplorer> {
highlightColor: Colors.transparent,
onPressed: () => _sendFile(file),
),
IconButton(
icon: const Icon(Icons.delete, color: AppTheme.secondaryText),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () => _deleteFile(file),
),
],
),
),
@@ -544,9 +725,9 @@ class _FileExplorerState extends State<FileExplorer> {
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onPressed: () {
final currentPath = '/';
final parentPath = _getParentPath(state.currentPath);
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: 'org1', path: currentPath),
LoadDirectory(orgId: 'org1', path: parentPath),
);
},
),
@@ -591,8 +772,11 @@ class _FileExplorerState extends State<FileExplorer> {
color: AppTheme.primaryText,
),
onPressed: () {
final parentPath = _getParentPath(
state.currentPath,
);
context.read<FileBrowserBloc>().add(
LoadDirectory(orgId: 'org1', path: '/'),
LoadDirectory(orgId: 'org1', path: parentPath),
);
},
),
@@ -724,11 +908,11 @@ class _FileExplorerState extends State<FileExplorer> {
),
);
},
onAccept: (draggedFile) {
onAcceptWithDetails: (draggedFile) {
context.read<FileBrowserBloc>().add(
MoveFile(
orgId: 'org1',
sourcePath: draggedFile.path,
sourcePath: draggedFile.data.path,
targetPath: file.path,
),
);

View File

@@ -7,5 +7,6 @@ abstract class FileRepository {
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<void> renameFile(String orgId, String path, String newName);
Future<List<FileItem>> searchFiles(String orgId, String query);
}

View File

@@ -1,5 +1,6 @@
import '../models/file_item.dart';
import '../repositories/file_repository.dart';
import 'package:path/path.dart' as p;
class MockFileRepository implements FileRepository {
final List<FileItem> _files = [
@@ -244,6 +245,24 @@ class MockFileRepository implements FileRepository {
}
}
@override
Future<void> renameFile(String orgId, String path, String newName) async {
await Future.delayed(const Duration(seconds: 1));
final fileIndex = _files.indexWhere((f) => f.path == path);
if (fileIndex != -1) {
final file = _files[fileIndex];
final parentPath = p.dirname(path);
final newPath = parentPath == '.' ? '/$newName' : '$parentPath/$newName';
_files[fileIndex] = FileItem(
name: newName,
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));

View File

@@ -56,6 +56,13 @@ class FileService {
await _fileRepository.moveFile(orgId, sourcePath, targetPath);
}
Future<void> renameFile(String orgId, String path, String newName) async {
if (path.isEmpty || newName.isEmpty) {
throw Exception('Path and new name cannot be empty');
}
await _fileRepository.renameFile(orgId, path, newName);
}
Future<List<FileItem>> searchFiles(String orgId, String query) async {
if (query.isEmpty) {
return [];

View File

@@ -793,7 +793,7 @@ packages:
source: hosted
version: "2.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View File

@@ -37,13 +37,15 @@ dependencies:
logger: ^2.0.2
# Image Handling
cached_network_image: ^3.3.0
cached_network_image:
^3.3.0
# SVG Support
flutter_svg: ^2.0.9
# Utilities
equatable: ^2.0.5
path: ^1.9.0
path_provider: ^2.1.2
connectivity_plus: ^5.0.2
provider: ^6.1.1