delete files and folders
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -793,7 +793,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user