Files
Client/lib/view/pages/share_intent/share_folder_picker.dart
T

257 lines
8.3 KiB
Dart

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<void> Function(BuildContext context, List<String> 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<FilesBloc, LoadableState<FilesState>>(
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<SettingsCubit>();
_currentSort = _settings.val().fileSettings.sortBy;
_ascending = _settings.val().fileSettings.ascending;
}
void _enter(FilesBloc bloc, List<String> currentPath, String folderName) {
bloc.setPath([...currentPath, folderName]);
}
void _goUp(FilesBloc bloc, List<String> currentPath) {
if (currentPath.isEmpty) return;
bloc.setPath(currentPath.sublist(0, currentPath.length - 1));
}
@override
Widget build(BuildContext context) {
final bloc = context.read<FilesBloc>();
return BlocBuilder<FilesBloc, LoadableState<FilesState>>(
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<String> 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<FilesBloc, FilesState>(
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<void> _externalUploadFlow(
BuildContext context,
List<String> 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<String> 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<String> targetPath) {
Navigator.of(context).popUntil((route) => route.isFirst);
AppRoutes.openFolder(context, targetPath);
}
Future<void> _internalCopyFlow(
BuildContext context,
List<String> 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);
}