third commit
This commit is contained in:
@@ -9,6 +9,10 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
String _currentOrgId = '';
|
String _currentOrgId = '';
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
List<FileItem> _currentFiles = [];
|
List<FileItem> _currentFiles = [];
|
||||||
|
List<FileItem> _filteredFiles = [];
|
||||||
|
int _currentPage = 1;
|
||||||
|
int _pageSize = 20;
|
||||||
|
String _sortBy = 'name';
|
||||||
|
|
||||||
FileBrowserBloc(this._fileService) : super(DirectoryInitial()) {
|
FileBrowserBloc(this._fileService) : super(DirectoryInitial()) {
|
||||||
on<LoadDirectory>(_onLoadDirectory);
|
on<LoadDirectory>(_onLoadDirectory);
|
||||||
@@ -18,6 +22,9 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
on<ApplyFilter>(_onApplyFilter);
|
on<ApplyFilter>(_onApplyFilter);
|
||||||
on<CreateFolder>(_onCreateFolder);
|
on<CreateFolder>(_onCreateFolder);
|
||||||
on<ResetFileBrowser>(_onResetFileBrowser);
|
on<ResetFileBrowser>(_onResetFileBrowser);
|
||||||
|
on<LoadPage>(_onLoadPage);
|
||||||
|
on<ChangePageSize>(_onChangePageSize);
|
||||||
|
on<SearchFiles>(_onSearchFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLoadDirectory(
|
void _onLoadDirectory(
|
||||||
@@ -29,19 +36,13 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
_currentPath = event.path;
|
_currentPath = event.path;
|
||||||
try {
|
try {
|
||||||
final files = await _fileService.getFiles(event.orgId, event.path);
|
final files = await _fileService.getFiles(event.orgId, event.path);
|
||||||
final breadcrumbs = _generateBreadcrumbs(event.path);
|
_currentFiles = _sortFiles(files, _sortBy);
|
||||||
_currentFiles = files;
|
_filteredFiles = _currentFiles;
|
||||||
|
_currentPage = 1;
|
||||||
if (files.isEmpty) {
|
if (files.isEmpty) {
|
||||||
emit(DirectoryEmpty());
|
emit(DirectoryEmpty());
|
||||||
} else {
|
} else {
|
||||||
emit(
|
_emitLoadedState(emit);
|
||||||
DirectoryLoaded(
|
|
||||||
files: files,
|
|
||||||
filteredFiles: files,
|
|
||||||
breadcrumbs: breadcrumbs,
|
|
||||||
currentPath: event.path,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(DirectoryError(e.toString()));
|
emit(DirectoryError(e.toString()));
|
||||||
@@ -64,24 +65,21 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onApplySort(ApplySort event, Emitter<FileBrowserState> emit) {
|
void _onApplySort(ApplySort event, Emitter<FileBrowserState> emit) {
|
||||||
// Implement sorting
|
_sortBy = event.sortBy;
|
||||||
// For now, just refresh
|
_currentFiles = _sortFiles(_currentFiles, _sortBy);
|
||||||
add(RefreshDirectory());
|
_filteredFiles = _currentFiles
|
||||||
|
.where((f) => f.name.toLowerCase().contains(''))
|
||||||
|
.toList(); // Re-apply filter if any, but since filter is separate, perhaps need to track filter
|
||||||
|
_currentPage = 1;
|
||||||
|
_emitLoadedState(emit);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onApplyFilter(ApplyFilter event, Emitter<FileBrowserState> emit) {
|
void _onApplyFilter(ApplyFilter event, Emitter<FileBrowserState> emit) {
|
||||||
// Implement filtering
|
_filteredFiles = _currentFiles
|
||||||
final filtered = _currentFiles
|
|
||||||
.where((f) => f.name.toLowerCase().contains(event.filter.toLowerCase()))
|
.where((f) => f.name.toLowerCase().contains(event.filter.toLowerCase()))
|
||||||
.toList();
|
.toList();
|
||||||
emit(
|
_currentPage = 1;
|
||||||
DirectoryLoaded(
|
_emitLoadedState(emit);
|
||||||
files: _currentFiles,
|
|
||||||
filteredFiles: filtered,
|
|
||||||
breadcrumbs: _generateBreadcrumbs(_currentPath),
|
|
||||||
currentPath: _currentPath,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onCreateFolder(
|
void _onCreateFolder(
|
||||||
@@ -110,6 +108,77 @@ class FileBrowserBloc extends Bloc<FileBrowserEvent, FileBrowserState> {
|
|||||||
emit(DirectoryInitial());
|
emit(DirectoryInitial());
|
||||||
_currentOrgId = '';
|
_currentOrgId = '';
|
||||||
_currentPath = '/';
|
_currentPath = '/';
|
||||||
|
_currentPage = 1;
|
||||||
|
_pageSize = 20;
|
||||||
|
_sortBy = 'name';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLoadPage(LoadPage event, Emitter<FileBrowserState> emit) {
|
||||||
|
_currentPage = event.page;
|
||||||
|
_emitLoadedState(emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChangePageSize(ChangePageSize event, Emitter<FileBrowserState> emit) {
|
||||||
|
_pageSize = event.pageSize;
|
||||||
|
_currentPage = 1; // Reset to first page
|
||||||
|
_emitLoadedState(emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearchFiles(SearchFiles event, Emitter<FileBrowserState> emit) async {
|
||||||
|
emit(DirectoryLoading());
|
||||||
|
try {
|
||||||
|
final files = await _fileService.searchFiles(event.orgId, event.query);
|
||||||
|
_currentFiles = files;
|
||||||
|
_filteredFiles = files;
|
||||||
|
_currentPage = 1;
|
||||||
|
if (files.isEmpty) {
|
||||||
|
emit(DirectoryEmpty());
|
||||||
|
} else {
|
||||||
|
_emitLoadedState(emit);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(DirectoryError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _emitLoadedState(Emitter<FileBrowserState> emit) {
|
||||||
|
final paginatedFiles = _getPaginatedFiles();
|
||||||
|
final totalPages = (_filteredFiles.length / _pageSize).ceil();
|
||||||
|
emit(
|
||||||
|
DirectoryLoaded(
|
||||||
|
files: _currentFiles,
|
||||||
|
filteredFiles: _filteredFiles,
|
||||||
|
paginatedFiles: paginatedFiles,
|
||||||
|
breadcrumbs: _generateBreadcrumbs(_currentPath),
|
||||||
|
currentPath: _currentPath,
|
||||||
|
currentPage: _currentPage,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
totalPages: totalPages,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileItem> _getPaginatedFiles() {
|
||||||
|
final start = (_currentPage - 1) * _pageSize;
|
||||||
|
final end = start + _pageSize;
|
||||||
|
return _filteredFiles.sublist(start, end.clamp(0, _filteredFiles.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileItem> _sortFiles(List<FileItem> files, String sortBy) {
|
||||||
|
final sorted = List<FileItem>.from(files);
|
||||||
|
sorted.sort((a, b) {
|
||||||
|
switch (sortBy) {
|
||||||
|
case 'name':
|
||||||
|
return a.name.compareTo(b.name);
|
||||||
|
case 'date':
|
||||||
|
return b.lastModified.compareTo(a.lastModified);
|
||||||
|
case 'size':
|
||||||
|
return b.size.compareTo(a.size);
|
||||||
|
default:
|
||||||
|
return a.name.compareTo(b.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Breadcrumb> _generateBreadcrumbs(String path) {
|
List<Breadcrumb> _generateBreadcrumbs(String path) {
|
||||||
|
|||||||
@@ -62,3 +62,31 @@ class CreateFolder extends FileBrowserEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ResetFileBrowser extends FileBrowserEvent {}
|
class ResetFileBrowser extends FileBrowserEvent {}
|
||||||
|
|
||||||
|
class LoadPage extends FileBrowserEvent {
|
||||||
|
final int page;
|
||||||
|
|
||||||
|
const LoadPage(this.page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [page];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangePageSize extends FileBrowserEvent {
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const ChangePageSize(this.pageSize);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [pageSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchFiles extends FileBrowserEvent {
|
||||||
|
final String orgId;
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
const SearchFiles({required this.orgId, required this.query});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [orgId, query];
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,18 +25,35 @@ class DirectoryLoading extends FileBrowserState {}
|
|||||||
class DirectoryLoaded extends FileBrowserState {
|
class DirectoryLoaded extends FileBrowserState {
|
||||||
final List<FileItem> files;
|
final List<FileItem> files;
|
||||||
final List<FileItem> filteredFiles;
|
final List<FileItem> filteredFiles;
|
||||||
|
final List<FileItem> paginatedFiles;
|
||||||
final List<Breadcrumb> breadcrumbs;
|
final List<Breadcrumb> breadcrumbs;
|
||||||
final String currentPath;
|
final String currentPath;
|
||||||
|
final int currentPage;
|
||||||
|
final int pageSize;
|
||||||
|
final int totalPages;
|
||||||
|
|
||||||
const DirectoryLoaded({
|
const DirectoryLoaded({
|
||||||
required this.files,
|
required this.files,
|
||||||
required this.filteredFiles,
|
required this.filteredFiles,
|
||||||
|
required this.paginatedFiles,
|
||||||
required this.breadcrumbs,
|
required this.breadcrumbs,
|
||||||
required this.currentPath,
|
required this.currentPath,
|
||||||
|
required this.currentPage,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.totalPages,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [files, filteredFiles, breadcrumbs, currentPath];
|
List<Object> get props => [
|
||||||
|
files,
|
||||||
|
filteredFiles,
|
||||||
|
paginatedFiles,
|
||||||
|
breadcrumbs,
|
||||||
|
currentPath,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryEmpty extends FileBrowserState {}
|
class DirectoryEmpty extends FileBrowserState {}
|
||||||
|
|||||||
@@ -7,22 +7,28 @@ import 'blocs/organization/organization_bloc.dart';
|
|||||||
import 'blocs/permission/permission_bloc.dart';
|
import 'blocs/permission/permission_bloc.dart';
|
||||||
import 'blocs/file_browser/file_browser_bloc.dart';
|
import 'blocs/file_browser/file_browser_bloc.dart';
|
||||||
import 'blocs/upload/upload_bloc.dart';
|
import 'blocs/upload/upload_bloc.dart';
|
||||||
import 'services/file_service.dart';
|
|
||||||
import 'repositories/mock_file_repository.dart';
|
import 'repositories/mock_file_repository.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'services/file_service.dart';
|
||||||
import 'pages/home_page.dart';
|
import 'pages/home_page.dart';
|
||||||
import 'pages/login_form.dart';
|
import 'pages/login_form.dart';
|
||||||
|
import 'pages/file_explorer.dart';
|
||||||
import 'pages/document_viewer.dart';
|
import 'pages/document_viewer.dart';
|
||||||
|
import 'theme/app_theme.dart';
|
||||||
|
|
||||||
final GoRouter _router = GoRouter(
|
final GoRouter _router = GoRouter(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(path: '/', builder: (context, state) => const HomePage()),
|
GoRoute(path: '/', builder: (context, state) => const HomePage()),
|
||||||
GoRoute(path: '/login', builder: (context, state) => const LoginForm()),
|
GoRoute(path: '/login', builder: (context, state) => const LoginForm()),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/viewer/:fileId',
|
path: '/viewer/:fileId',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
DocumentViewer(fileId: state.pathParameters['fileId']!),
|
DocumentViewer(fileId: state.pathParameters['fileId']!),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/org/:orgId/drive',
|
||||||
|
builder: (context, state) => const FileExplorer(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import '../blocs/file_browser/file_browser_bloc.dart';
|
|||||||
import '../blocs/file_browser/file_browser_event.dart';
|
import '../blocs/file_browser/file_browser_event.dart';
|
||||||
import '../blocs/file_browser/file_browser_state.dart';
|
import '../blocs/file_browser/file_browser_state.dart';
|
||||||
import '../blocs/permission/permission_bloc.dart';
|
import '../blocs/permission/permission_bloc.dart';
|
||||||
|
import '../blocs/permission/permission_event.dart';
|
||||||
import '../blocs/permission/permission_state.dart';
|
import '../blocs/permission/permission_state.dart';
|
||||||
import '../blocs/upload/upload_bloc.dart';
|
import '../blocs/upload/upload_bloc.dart';
|
||||||
import '../blocs/upload/upload_event.dart';
|
import '../blocs/upload/upload_event.dart';
|
||||||
import '../models/file_item.dart';
|
import '../models/file_item.dart';
|
||||||
|
import '../theme/app_theme.dart';
|
||||||
|
import '../theme/modern_glass_button.dart';
|
||||||
|
|
||||||
class FileExplorer extends StatefulWidget {
|
class FileExplorer extends StatefulWidget {
|
||||||
const FileExplorer({super.key});
|
const FileExplorer({super.key});
|
||||||
@@ -19,6 +22,18 @@ class FileExplorer extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FileExplorerState extends State<FileExplorer> {
|
class _FileExplorerState extends State<FileExplorer> {
|
||||||
|
String? _selectedFilePath;
|
||||||
|
bool _isSearching = false;
|
||||||
|
bool _showField = false;
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -26,6 +41,217 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
LoadDirectory(orgId: 'org1', path: '/'),
|
LoadDirectory(orgId: 'org1', path: '/'),
|
||||||
);
|
);
|
||||||
|
context.read<PermissionBloc>().add(LoadPermissions('org1'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _showCreateFolderDialog(BuildContext context) async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
return showDialog<String>(
|
||||||
|
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(
|
||||||
|
'New Folder',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Enter folder 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: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.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 folderName = controller.text.trim();
|
||||||
|
if (folderName.isNotEmpty) {
|
||||||
|
final nameWithSlash = folderName.startsWith('/')
|
||||||
|
? folderName
|
||||||
|
: '/$folderName';
|
||||||
|
Navigator.of(context).pop(nameWithSlash);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Create',
|
||||||
|
style: TextStyle(color: AppTheme.accentColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}')));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle() {
|
||||||
|
const double titleWidth = 72.0;
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
child: const Text(
|
||||||
|
'/Drive',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedPositioned(
|
||||||
|
left: _isSearching ? titleWidth + 250.0 : titleWidth,
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isSearching ? Icons.close : Icons.search,
|
||||||
|
color: AppTheme.accentColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (_isSearching) {
|
||||||
|
setState(() {
|
||||||
|
_showField = false;
|
||||||
|
_isSearching = false;
|
||||||
|
_searchController.clear();
|
||||||
|
_searchQuery = '';
|
||||||
|
context.read<FileBrowserBloc>().add(ApplyFilter(''));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_isSearching = true;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 150), () {
|
||||||
|
setState(() {
|
||||||
|
_showField = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showField)
|
||||||
|
AnimatedPositioned(
|
||||||
|
left: titleWidth,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
autofocus: true,
|
||||||
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search Files...',
|
||||||
|
hintStyle: const TextStyle(color: AppTheme.secondaryText),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 2,
|
||||||
|
horizontal: 12,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: AppTheme.secondaryText.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
borderSide: BorderSide(color: AppTheme.accentColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_searchQuery = value;
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
ApplyFilter(_searchQuery),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,7 +265,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error: ${state.error}',
|
'Error: ${state.error}',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: AppTheme.primaryText),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,16 +275,25 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Container(
|
||||||
'Drive',
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(fontSize: 24, color: Colors.white),
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: AppTheme.primaryText.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _buildTitle(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Back button
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: const Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: AppTheme.primaryText,
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
LoadDirectory(orgId: 'org1', path: '/'),
|
LoadDirectory(orgId: 'org1', path: '/'),
|
||||||
@@ -67,7 +302,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
),
|
),
|
||||||
const Text(
|
const Text(
|
||||||
'Empty Folder',
|
'Empty Folder',
|
||||||
style: TextStyle(color: Colors.white),
|
style: TextStyle(color: AppTheme.primaryText),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -76,9 +311,12 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
builder: (context, permState) {
|
builder: (context, permState) {
|
||||||
if (permState is PermissionLoaded &&
|
if (permState is PermissionLoaded &&
|
||||||
permState.capabilities.canWrite) {
|
permState.capabilities.canWrite) {
|
||||||
return ElevatedButton(
|
return Row(
|
||||||
|
children: [
|
||||||
|
ModernGlassButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await FilePicker.platform.pickFiles();
|
final result = await FilePicker.platform
|
||||||
|
.pickFiles();
|
||||||
if (result != null && result.files.isNotEmpty) {
|
if (result != null && result.files.isNotEmpty) {
|
||||||
final files = result.files
|
final files = result.files
|
||||||
.map(
|
.map(
|
||||||
@@ -100,7 +338,39 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Upload File'),
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.upload),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Upload File'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ModernGlassButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final folderName = await _showCreateFolderDialog(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (folderName != null && folderName.isNotEmpty) {
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
CreateFolder(
|
||||||
|
orgId: 'org1',
|
||||||
|
parentPath: '/',
|
||||||
|
folderName: folderName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.create_new_folder),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('New Folder'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -116,9 +386,16 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Container(
|
||||||
'Drive',
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(fontSize: 24, color: Colors.white),
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: AppTheme.primaryText.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _buildTitle(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Breadcrumbs and back button
|
// Breadcrumbs and back button
|
||||||
@@ -131,7 +408,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back,
|
Icons.arrow_back,
|
||||||
color: Colors.white,
|
color: AppTheme.primaryText,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
@@ -153,7 +430,7 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'${breadcrumb.name}/',
|
'${breadcrumb.name}/',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white70,
|
color: AppTheme.secondaryText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -171,9 +448,12 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
builder: (context, permState) {
|
builder: (context, permState) {
|
||||||
if (permState is PermissionLoaded &&
|
if (permState is PermissionLoaded &&
|
||||||
permState.capabilities.canWrite) {
|
permState.capabilities.canWrite) {
|
||||||
return ElevatedButton(
|
return Row(
|
||||||
|
children: [
|
||||||
|
ModernGlassButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final result = await FilePicker.platform.pickFiles();
|
final result = await FilePicker.platform
|
||||||
|
.pickFiles();
|
||||||
if (result != null && result.files.isNotEmpty) {
|
if (result != null && result.files.isNotEmpty) {
|
||||||
final files = result.files
|
final files = result.files
|
||||||
.map(
|
.map(
|
||||||
@@ -195,7 +475,40 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Upload File'),
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.upload),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Upload File'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ModernGlassButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final folderName = await _showCreateFolderDialog(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (folderName != null && folderName.isNotEmpty) {
|
||||||
|
context.read<FileBrowserBloc>().add(
|
||||||
|
CreateFolder(
|
||||||
|
orgId: 'org1',
|
||||||
|
parentPath: state.currentPath,
|
||||||
|
folderName: folderName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.create_new_folder),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('New Folder'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
@@ -204,27 +517,15 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: state.files.length,
|
itemCount: state.filteredFiles.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final file = state.files[index];
|
final file = state.filteredFiles[index];
|
||||||
return ListTile(
|
final isSelected = _selectedFilePath == file.path;
|
||||||
leading: Icon(
|
return GestureDetector(
|
||||||
file.type == FileType.folder
|
|
||||||
? Icons.folder
|
|
||||||
: Icons.insert_drive_file,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
file.name,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
file.type == FileType.folder
|
|
||||||
? 'Folder'
|
|
||||||
: 'File - ${file.size} bytes',
|
|
||||||
style: const TextStyle(color: Colors.white70),
|
|
||||||
),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedFilePath = file.path;
|
||||||
|
});
|
||||||
if (file.type == FileType.folder) {
|
if (file.type == FileType.folder) {
|
||||||
context.read<FileBrowserBloc>().add(
|
context.read<FileBrowserBloc>().add(
|
||||||
NavigateToFolder(file.path),
|
NavigateToFolder(file.path),
|
||||||
@@ -234,6 +535,78 @@ class _FileExplorerState extends State<FileExplorer> {
|
|||||||
context.go('/viewer/${file.name}');
|
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)
|
||||||
|
: 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,
|
||||||
|
),
|
||||||
|
onPressed: () => _editFile(file),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.download,
|
||||||
|
color: AppTheme.secondaryText,
|
||||||
|
),
|
||||||
|
onPressed: () => _downloadFile(file),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.send,
|
||||||
|
color: AppTheme.secondaryText,
|
||||||
|
),
|
||||||
|
onPressed: () => _sendFile(file),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ abstract class FileRepository {
|
|||||||
Future<void> uploadFile(String orgId, FileItem file);
|
Future<void> uploadFile(String orgId, FileItem file);
|
||||||
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<List<FileItem>> searchFiles(String orgId, String query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,4 +78,12 @@ class MockFileRepository implements FileRepository {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
return _files
|
||||||
|
.where((f) => f.name.toLowerCase().contains(query.toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,4 +44,11 @@ class FileService {
|
|||||||
}
|
}
|
||||||
await _fileRepository.createFolder(orgId, parentPath, folderName);
|
await _fileRepository.createFolder(orgId, parentPath, folderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await _fileRepository.searchFiles(orgId, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user