import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import '../../../api/errors/error_mapper.dart'; import '../../../routing/app_routes.dart'; import '../../../share_intent/internal_share_actions.dart'; import '../../../share_intent/pending_share.dart'; import '../../../share_intent/remote_file_ref.dart'; import '../../../share_intent/share_intent_listener.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 '../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../widget/info_dialog.dart'; import '../../../widget/placeholder_view.dart'; import '../files/data/sort_options.dart'; import '../files/files_upload_dialog.dart'; import '../files/widgets/add_file_menu.dart'; import '../files/widgets/files_sort_actions.dart'; typedef _FolderConfirmedCallback = Future Function(BuildContext context, List targetPath); class ShareFolderPicker extends StatelessWidget { final String _fabLabel; final _FolderConfirmedCallback _onConfirm; const ShareFolderPicker._({ required String fabLabel, required _FolderConfirmedCallback onConfirm, }) : _fabLabel = fabLabel, _onConfirm = onConfirm; /// External share-intent flow: upload local files into the chosen folder. factory ShareFolderPicker.forExternalShare({required PendingShare share}) => ShareFolderPicker._( fabLabel: 'Hier hochladen', onConfirm: (ctx, target) => _externalUploadFlow(ctx, target, share), ); /// In-app save flow: server-to-server WebDAV-copy of an existing file into /// the chosen folder. factory ShareFolderPicker.forInternalSave({required RemoteFileRef file}) => ShareFolderPicker._( fabLabel: 'Hierhin kopieren', onConfirm: (ctx, target) => _internalCopyFlow(ctx, target, file), ); @override Widget build(BuildContext context) => BlocModule>( create: (_) => FilesBloc(), child: (context, _, _) => _ShareFolderPickerView(fabLabel: _fabLabel, onConfirm: _onConfirm), ); } class _ShareFolderPickerView extends StatefulWidget { final String fabLabel; final _FolderConfirmedCallback onConfirm; const _ShareFolderPickerView({ required this.fabLabel, required this.onConfirm, }); @override State<_ShareFolderPickerView> createState() => _ShareFolderPickerViewState(); } class _ShareFolderPickerViewState extends State<_ShareFolderPickerView> { late final SettingsCubit _settings; late SortOption _currentSort; late bool _ascending; @override void initState() { super.initState(); _settings = context.read(); _currentSort = _settings.val().fileSettings.sortBy; _ascending = _settings.val().fileSettings.ascending; } void _enter(FilesBloc bloc, List currentPath, String folderName) { bloc.setPath([...currentPath, folderName]); } void _goUp(FilesBloc bloc, List currentPath) { if (currentPath.isEmpty) return; bloc.setPath(currentPath.sublist(0, currentPath.length - 1)); } @override Widget build(BuildContext context) { final bloc = context.read(); return BlocBuilder>( buildWhen: (a, b) => a.data?.currentPath != b.data?.currentPath, builder: (_, outerState) { final currentPath = outerState.data?.currentPath ?? const []; return PopScope( // Back navigates one level up while inside a sub-folder; only the // root level actually closes the picker. Matches the standard // files-app pattern and keeps the AppBar back-arrow consistent // with the chat picker. canPop: currentPath.isEmpty, onPopInvokedWithResult: (didPop, _) { if (didPop) return; if (currentPath.isNotEmpty) _goUp(bloc, currentPath); }, child: _buildScaffold(context, bloc, currentPath), ); }, ); } Widget _buildScaffold( BuildContext context, FilesBloc bloc, List currentPath, ) => Scaffold( appBar: AppBar( title: Text( currentPath.isEmpty ? 'Ordner wählen' : '/${currentPath.join('/')}', overflow: TextOverflow.ellipsis, ), actions: [ IconButton( icon: const Icon(Icons.create_new_folder_outlined), tooltip: 'Ordner erstellen', onPressed: () => showCreateFolderDialog(context, bloc), ), FilesSortActions( currentSort: _currentSort, ascending: _ascending, onDirectionChanged: (e) { setState(() { _ascending = e; _settings.val(write: true).fileSettings.ascending = e; }); }, onSortChanged: (e) { setState(() { _currentSort = e; _settings.val(write: true).fileSettings.sortBy = e; }); }, ), ], ), floatingActionButton: FloatingActionButton.extended( heroTag: 'shareUploadHere', onPressed: () => widget.onConfirm(context, currentPath), icon: const Icon(Icons.upload), label: Text(widget.fabLabel), ), body: LoadableStateConsumer( isReady: (state) => state.listing != null, child: (state, _) { final listing = state.listing!; final entries = listing.sortBy( sortOption: _currentSort, foldersToTop: _settings.val().fileSettings.sortFoldersToTop, reversed: _ascending, ); if (entries.isEmpty) { return PlaceholderView( icon: Icons.folder_off_rounded, text: state.currentPath.isEmpty ? 'Leer. Du kannst hier direkt hochladen.' : 'Ordner ist leer. Du kannst hier hochladen.', ); } return ListView.builder( padding: EdgeInsets.zero, itemCount: entries.length, itemBuilder: (context, i) { final entry = entries[i]; if (entry.isDirectory) { return ListTile( leading: const Icon(Icons.folder_outlined), title: Text(entry.name), trailing: const Icon(Icons.chevron_right), onTap: () => _enter(bloc, state.currentPath, entry.name), ); } return ListTile( enabled: false, leading: const Icon(Icons.description_outlined), title: Text(entry.name), ); }, ); }, ), ); } Future _externalUploadFlow( BuildContext context, List targetPath, PendingShare share, ) async { await pushScreen( context, withNavBar: false, screen: FilesUploadDialog( filePaths: share.filePaths, remotePath: targetPath.join('/'), onUploadFinished: (_) => _afterExternalUploaded(context, targetPath), ), ); } void _afterExternalUploaded(BuildContext context, List targetPath) { ShareIntentListener.instance.clear(); if (!context.mounted) return; _finishWithFolder(context, targetPath); } /// Closes any picker pages stacked on top of the current tab and jumps to /// the chosen folder. Shared by external upload + internal copy flows. void _finishWithFolder(BuildContext context, List targetPath) { Navigator.of(context).popUntil((route) => route.isFirst); AppRoutes.openFolder(context, targetPath); } Future _internalCopyFlow( BuildContext context, List targetPath, RemoteFileRef file, ) async { final bool ok; try { ok = await copyRemoteFileTo( context: context, source: file, targetFolderPath: targetPath.join('/'), ); } catch (e) { if (context.mounted) { InfoDialog.show( context, errorToUserMessage(e), title: 'Kopieren fehlgeschlagen', copyable: true, ); } return; } if (!ok || !context.mounted) return; _finishWithFolder(context, targetPath); }