delete files and folders
This commit is contained in:
@@ -3,6 +3,7 @@ import 'file_browser_event.dart';
|
|||||||
import 'file_browser_state.dart';
|
import 'file_browser_state.dart';
|
||||||
import '../../services/file_service.dart';
|
import '../../services/file_service.dart';
|
||||||
import '../../models/file_item.dart';
|
import '../../models/file_item.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
||||||
final FileService _fileService;
|
final FileService _fileService;
|
||||||
@@ -24,6 +25,8 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
on<ApplyFilter>(_onApplyFilter);
|
on<ApplyFilter>(_onApplyFilter);
|
||||||
on<CreateFolder>(_onCreateFolder);
|
on<CreateFolder>(_onCreateFolder);
|
||||||
on<MoveFile>(_onMoveFile);
|
on<MoveFile>(_onMoveFile);
|
||||||
|
on<RenameFile>(_onRenameFile);
|
||||||
|
on<DeleteFile>(_onDeleteFile);
|
||||||
on<ResetFileBrowser>(_onResetFileBrowser);
|
on<ResetFileBrowser>(_onResetFileBrowser);
|
||||||
on<LoadPage>(_onLoadPage);
|
on<LoadPage>(_onLoadPage);
|
||||||
on<ChangePageSize>(_onChangePageSize);
|
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(
|
void _onResetFileBrowser(
|
||||||
ResetFileBrowser event,
|
ResetFileBrowser event,
|
||||||
Emitter<FileBrowserState> emit,
|
Emitter<FileBrowserState> emit,
|
||||||
|
|||||||
@@ -106,3 +106,28 @@ class MoveFile extends FileBrowserEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object> get props => [orgId, sourcePath, targetPath];
|
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 = '';
|
String _searchQuery = '';
|
||||||
final Map<String, bool> _hovered = {};
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
@@ -142,9 +150,110 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _editFile(FileItem file) {
|
void _editFile(FileItem file) {
|
||||||
ScaffoldMessenger.of(
|
_showRenameDialog(file);
|
||||||
context,
|
}
|
||||||
).showSnackBar(SnackBar(content: Text('Edit ${file.name}')));
|
|
||||||
|
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) {
|
void _downloadFile(FileItem file) {
|
||||||
@@ -159,6 +268,72 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
).showSnackBar(SnackBar(content: Text('Send ${file.name}')));
|
).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() {
|
Widget _buildTitle() {
|
||||||
const double titleWidth = 72.0;
|
const double titleWidth = 72.0;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@@ -419,6 +594,12 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
onPressed: () => _sendFile(file),
|
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,
|
splashColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final currentPath = '/';
|
final parentPath = _getParentPath(state.currentPath);
|
||||||
context.read<FileBrowserBloc>().add(
|
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,
|
color: AppTheme.primaryText,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final parentPath = _getParentPath(
|
||||||
|
state.currentPath,
|
||||||
|
);
|
||||||
context.read<FileBrowserBloc>().add(
|
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(
|
context.read<FileBrowserBloc>().add(
|
||||||
MoveFile(
|
MoveFile(
|
||||||
orgId: 'org1',
|
orgId: 'org1',
|
||||||
sourcePath: draggedFile.path,
|
sourcePath: draggedFile.data.path,
|
||||||
targetPath: file.path,
|
targetPath: file.path,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ abstract class FileRepository {
|
|||||||
Future<void> deleteFile(String orgId, String path);
|
Future<void> deleteFile(String orgId, String path);
|
||||||
Future<void> createFolder(String orgId, String parentPath, String folderName);
|
Future<void> createFolder(String orgId, String parentPath, String folderName);
|
||||||
Future<void> moveFile(String orgId, String sourcePath, String targetPath);
|
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);
|
Future<List<FileItem>> searchFiles(String orgId, String query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import '../models/file_item.dart';
|
import '../models/file_item.dart';
|
||||||
import '../repositories/file_repository.dart';
|
import '../repositories/file_repository.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
class MockFileRepository implements FileRepository {
|
class MockFileRepository implements FileRepository {
|
||||||
final List<FileItem> _files = [
|
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
|
@override
|
||||||
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ class FileService {
|
|||||||
await _fileRepository.moveFile(orgId, sourcePath, targetPath);
|
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 {
|
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -793,7 +793,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
|||||||
@@ -37,13 +37,15 @@ dependencies:
|
|||||||
logger: ^2.0.2
|
logger: ^2.0.2
|
||||||
|
|
||||||
# Image Handling
|
# Image Handling
|
||||||
cached_network_image: ^3.3.0
|
cached_network_image:
|
||||||
|
^3.3.0
|
||||||
|
|
||||||
# SVG Support
|
# SVG Support
|
||||||
flutter_svg: ^2.0.9
|
flutter_svg: ^2.0.9
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
|
path: ^1.9.0
|
||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
connectivity_plus: ^5.0.2
|
connectivity_plus: ^5.0.2
|
||||||
provider: ^6.1.1
|
provider: ^6.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user