import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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 '../../../widget/centered_leading.dart'; import '../../../widget/info_dialog.dart'; import '../../../widget/placeholder_view.dart'; import '../files/data/file_sort.dart'; import '../files/widgets/add_file_menu.dart'; import '../files/widgets/file_leading.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> { 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(); // 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 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; _goUp(bloc, currentPath); }, child: _buildScaffold(context, bloc, currentPath, sort), ); }, ); } Widget _buildScaffold( BuildContext context, FilesBloc bloc, List currentPath, FileSort sort, ) => 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), ), const FilesSortActions(), ], ), 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 entries = state.listing!.sortedBy(sort); 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]; return ListTile( enabled: entry.isDirectory, leading: CenteredLeading(FileLeading(file: entry)), title: Text(entry.name), trailing: entry.isDirectory ? const Icon(Icons.chevron_right) : null, onTap: entry.isDirectory ? () => bloc.setPath([...state.currentPath, entry.name]) : null, ); }, ); }, ), ); } Future _externalUploadFlow( BuildContext context, List targetPath, PendingShare share, ) async { ShareIntentListener.instance.beginFlow(); try { await AppRoutes.openFilesUpload( context, filePaths: share.filePaths, remotePath: targetPath.join('/'), onUploadFinished: (_) => _afterExternalUploaded(context, targetPath, share), ); } finally { ShareIntentListener.instance.endFlow(); } } void _afterExternalUploaded( BuildContext context, List targetPath, PendingShare share, ) { ShareIntentListener.instance.clear(ifCurrent: share); 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); }