import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart'; import '../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart'; import '../../../routing/app_routes.dart'; import '../../../state/app/infrastructure/loadable_state/loadable_state.dart'; import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart'; import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart'; import '../../../state/app/modules/files/bloc/files_bloc.dart'; import '../../../state/app/modules/files/bloc/files_state.dart'; import '../../../utils/cache_invalidation_bus.dart'; import '../../../widget/placeholder_view.dart'; import 'data/file_sort.dart'; import 'data/webdav_path.dart'; import 'search/files_search_delegate.dart'; import 'widgets/add_file_menu.dart'; import 'widgets/clipboard_banner.dart'; import 'widgets/file_element.dart'; import 'widgets/files_sort_actions.dart'; class Files extends StatelessWidget { final List path; Files({List? path, super.key}) : path = path ?? []; @override Widget build(BuildContext context) => BlocModule>( create: (_) => FilesBloc(initialPath: path), child: (context, _, _) => _FilesView(path: path), ); } class _FilesView extends StatefulWidget { final List path; const _FilesView({required this.path}); @override State<_FilesView> createState() => _FilesViewState(); } class _FilesViewState extends State<_FilesView> { late final StreamSubscription _invalidationSub; // The list builder also runs for loading flips and parent rebuilds; only a // new listing or a changed sort needs another sort pass. Object? _sortedKey; List _sortedFiles = const []; List _sorted(ListFilesResponse listing, FileSort sort) { final key = (listing, sort); if (key == _sortedKey) return _sortedFiles; _sortedKey = key; return _sortedFiles = listing.sortedBy(sort); } @override void initState() { super.initState(); _invalidationSub = CacheInvalidationBus.listFilesStream.listen( _onInvalidation, ); } void _onInvalidation(String invalidatedPath) { if (!mounted) return; if (invalidatedPath != listFilesCacheKey(widget.path)) return; context.read().refresh(); } @override void dispose() { _invalidationSub.cancel(); super.dispose(); } Future _mediaUpload(List? paths) async { if (paths == null) return; final bloc = context.read(); unawaited( AppRoutes.openFilesUpload( context, filePaths: paths, remotePath: widget.path.join('/'), onUploadFinished: (_) => bloc.refresh(), ), ); } @override Widget build(BuildContext context) { final bloc = context.read(); // Selected here, not in the LoadableStateConsumer child: that closure runs // during the consumer's build, where this context may not subscribe. final sort = context.select(fileSortOf); return Scaffold( appBar: AppBar( title: Text(widget.path.isNotEmpty ? widget.path.last : 'Dateien'), actions: [ const FilesSortActions(), IconButton( tooltip: 'Suchen', icon: const Icon(Icons.search), onPressed: () async { final delegate = FilesSearchDelegate(pathScope: widget.path); await showSearch(context: context, delegate: delegate); delegate.disposeController(); }, ), ], ), floatingActionButton: FloatingActionButton( heroTag: 'uploadFile', backgroundColor: Theme.of(context).primaryColor, onPressed: () => showAddFileSheet(context, bloc: bloc, onPickedFiles: _mediaUpload), child: const Icon(Icons.add), ), body: Column( children: [ ClipboardBanner( currentFolder: webdavFolderPath(widget.path), onPasteDone: bloc.refresh, ), Expanded( child: LoadableStateConsumer( isReady: (state) => state.listing != null, child: (state, _) { final listing = state.listing!; if (listing.files.isEmpty) { return const PlaceholderView( icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer', ); } final files = _sorted(listing, sort); return ListView.builder( padding: EdgeInsets.zero, itemCount: files.length, itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh), ); }, ), ), ], ), ); } }