257 lines
8.3 KiB
Dart
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);
|
|
}
|