From d3b2c138ee75a2a25715780fa7f8615788124617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6sche?= Date: Wed, 17 Dec 2025 01:49:45 +0100 Subject: [PATCH] delete files and folders --- .../blocs/file_browser/file_browser_bloc.dart | 36 ++++ .../file_browser/file_browser_event.dart | 25 +++ b0esche_cloud/lib/pages/file_explorer.dart | 200 +++++++++++++++++- .../lib/repositories/file_repository.dart | 1 + .../repositories/mock_file_repository.dart | 19 ++ b0esche_cloud/lib/services/file_service.dart | 7 + b0esche_cloud/pubspec.lock | 2 +- b0esche_cloud/pubspec.yaml | 4 +- 8 files changed, 284 insertions(+), 10 deletions(-) diff --git a/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart b/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart index c2ca519..ca066eb 100644 --- a/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart +++ b/b0esche_cloud/lib/blocs/file_browser/file_browser_bloc.dart @@ -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 { final FileService _fileService; @@ -24,6 +25,8 @@ class FileBrowserBloc extends Bloc { on(_onApplyFilter); on(_onCreateFolder); on(_onMoveFile); + on(_onRenameFile); + on(_onDeleteFile); on(_onResetFileBrowser); on(_onLoadPage); on(_onChangePageSize); @@ -144,6 +147,39 @@ class FileBrowserBloc extends Bloc { } } + void _onRenameFile(RenameFile event, Emitter 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 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 emit, diff --git a/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart b/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart index 5084248..78bcdf6 100644 --- a/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart +++ b/b0esche_cloud/lib/blocs/file_browser/file_browser_event.dart @@ -106,3 +106,28 @@ class MoveFile extends FileBrowserEvent { @override List 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 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 get props => [orgId, path]; +} diff --git a/b0esche_cloud/lib/pages/file_explorer.dart b/b0esche_cloud/lib/pages/file_explorer.dart index c74eb0e..fe1d800 100644 --- a/b0esche_cloud/lib/pages/file_explorer.dart +++ b/b0esche_cloud/lib/pages/file_explorer.dart @@ -29,6 +29,14 @@ class _FileExplorerState extends State { String _searchQuery = ''; final Map _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 { } void _editFile(FileItem file) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Edit ${file.name}'))); + _showRenameDialog(file); + } + + Future _showRenameDialog(FileItem file) async { + final TextEditingController controller = TextEditingController( + text: file.name, + ); + return showDialog( + 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().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 { ).showSnackBar(SnackBar(content: Text('Send ${file.name}'))); } + Future _deleteFile(FileItem file) async { + final confirmed = await showDialog( + 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().add( + DeleteFile(orgId: 'org1', path: file.path), + ); + } + } + Widget _buildTitle() { const double titleWidth = 72.0; return SizedBox( @@ -419,6 +594,12 @@ class _FileExplorerState extends State { 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 { splashColor: Colors.transparent, highlightColor: Colors.transparent, onPressed: () { - final currentPath = '/'; + final parentPath = _getParentPath(state.currentPath); context.read().add( - LoadDirectory(orgId: 'org1', path: currentPath), + LoadDirectory(orgId: 'org1', path: parentPath), ); }, ), @@ -591,8 +772,11 @@ class _FileExplorerState extends State { color: AppTheme.primaryText, ), onPressed: () { + final parentPath = _getParentPath( + state.currentPath, + ); context.read().add( - LoadDirectory(orgId: 'org1', path: '/'), + LoadDirectory(orgId: 'org1', path: parentPath), ); }, ), @@ -724,11 +908,11 @@ class _FileExplorerState extends State { ), ); }, - onAccept: (draggedFile) { + onAcceptWithDetails: (draggedFile) { context.read().add( MoveFile( orgId: 'org1', - sourcePath: draggedFile.path, + sourcePath: draggedFile.data.path, targetPath: file.path, ), ); diff --git a/b0esche_cloud/lib/repositories/file_repository.dart b/b0esche_cloud/lib/repositories/file_repository.dart index 833af9b..a844bdb 100644 --- a/b0esche_cloud/lib/repositories/file_repository.dart +++ b/b0esche_cloud/lib/repositories/file_repository.dart @@ -7,5 +7,6 @@ abstract class FileRepository { Future deleteFile(String orgId, String path); Future createFolder(String orgId, String parentPath, String folderName); Future moveFile(String orgId, String sourcePath, String targetPath); + Future renameFile(String orgId, String path, String newName); Future> searchFiles(String orgId, String query); } diff --git a/b0esche_cloud/lib/repositories/mock_file_repository.dart b/b0esche_cloud/lib/repositories/mock_file_repository.dart index c3a87b1..b85801c 100644 --- a/b0esche_cloud/lib/repositories/mock_file_repository.dart +++ b/b0esche_cloud/lib/repositories/mock_file_repository.dart @@ -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 _files = [ @@ -244,6 +245,24 @@ class MockFileRepository implements FileRepository { } } + @override + Future 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> searchFiles(String orgId, String query) async { await Future.delayed(const Duration(seconds: 1)); diff --git a/b0esche_cloud/lib/services/file_service.dart b/b0esche_cloud/lib/services/file_service.dart index 801393f..6284a2c 100644 --- a/b0esche_cloud/lib/services/file_service.dart +++ b/b0esche_cloud/lib/services/file_service.dart @@ -56,6 +56,13 @@ class FileService { await _fileRepository.moveFile(orgId, sourcePath, targetPath); } + Future 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> searchFiles(String orgId, String query) async { if (query.isEmpty) { return []; diff --git a/b0esche_cloud/pubspec.lock b/b0esche_cloud/pubspec.lock index 6444d9f..c58883c 100644 --- a/b0esche_cloud/pubspec.lock +++ b/b0esche_cloud/pubspec.lock @@ -793,7 +793,7 @@ packages: source: hosted version: "2.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" diff --git a/b0esche_cloud/pubspec.yaml b/b0esche_cloud/pubspec.yaml index cccebae..64698e5 100644 --- a/b0esche_cloud/pubspec.yaml +++ b/b0esche_cloud/pubspec.yaml @@ -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