289 lines
9.4 KiB
Dart
289 lines
9.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../../../api/errors/error_mapper.dart';
|
|
import '../../../../api/marianumcloud/files_sharing/file_sharing_api.dart';
|
|
import '../../../../api/marianumcloud/files_sharing/file_sharing_api_params.dart';
|
|
import '../../../../api/marianumcloud/files_sharing/ocs_path.dart';
|
|
import '../../../../api/marianumcloud/files_sharing/queries/share/share.dart';
|
|
import '../../../../api/marianumcloud/files_sharing/share_permissions.dart';
|
|
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
|
import '../../../../routing/app_routes.dart';
|
|
import '../../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
|
|
import '../../../../utils/clipboard_helper.dart';
|
|
import '../../../../widget/app_progress_indicator.dart';
|
|
import '../../../../widget/centered_leading.dart';
|
|
import '../../../../widget/details_bottom_sheet.dart';
|
|
import '../../../../widget/info_dialog.dart';
|
|
import '../widgets/file_leading.dart';
|
|
import 'share_options_sheet.dart';
|
|
import 'share_password_sheet.dart';
|
|
|
|
/// Opens the sharing sheet for a file/folder: lists existing shares and offers
|
|
/// gated actions to create new ones (public link, user/group). Capability
|
|
/// gating mirrors the Nextcloud web UI.
|
|
void showShareSheet(BuildContext context, CacheableFile file) {
|
|
showDetailsBottomSheet(
|
|
context,
|
|
header: ListTile(
|
|
leading: CenteredLeading(FileLeading(file: file)),
|
|
title: Text(file.name),
|
|
subtitle: const Text('Freigeben'),
|
|
),
|
|
children: (sheetCtx) => [_ShareSheetBody(file: file)],
|
|
);
|
|
}
|
|
|
|
class _ShareSheetBody extends StatefulWidget {
|
|
final CacheableFile file;
|
|
|
|
const _ShareSheetBody({required this.file});
|
|
|
|
@override
|
|
State<_ShareSheetBody> createState() => _ShareSheetBodyState();
|
|
}
|
|
|
|
class _ShareSheetBodyState extends State<_ShareSheetBody> {
|
|
late final String _ocsPath = ocsPathOf(widget.file);
|
|
Future<List<Share>>? _future;
|
|
bool _busy = false;
|
|
|
|
/// Last resolved share list — used by the create-link gate without depending
|
|
/// on the FutureBuilder's snapshot.
|
|
List<Share>? _lastShares;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_future = FileSharingApi().listForPath(_ocsPath);
|
|
}
|
|
|
|
void _reload() {
|
|
// Block body: an arrow would return the Future, which setState rejects.
|
|
setState(() {
|
|
_future = FileSharingApi().listForPath(_ocsPath);
|
|
});
|
|
}
|
|
|
|
Future<void> _runCreate(FileSharingApiParams params) async {
|
|
setState(() => _busy = true);
|
|
try {
|
|
await FileSharingApi().share(params);
|
|
if (!mounted) return;
|
|
_reload();
|
|
} catch (e) {
|
|
if (mounted) {
|
|
InfoDialog.show(
|
|
context,
|
|
errorToUserMessage(e),
|
|
title: 'Freigabe fehlgeschlagen',
|
|
copyable: true,
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _createPublicLink() async {
|
|
final caps = context.read<NextcloudCapabilitiesCubit>();
|
|
String? password;
|
|
if (caps.passwordEnforced) {
|
|
// The server requires a password — collect it before creating, otherwise
|
|
// the call is rejected.
|
|
password = await promptSharePassword(
|
|
context,
|
|
isChange: false,
|
|
policyHint: caps.passwordPolicyHint,
|
|
);
|
|
if (password == null || password.isEmpty || !mounted) return;
|
|
}
|
|
await _runCreate(
|
|
FileSharingApiParams(
|
|
shareType: kShareTypePublicLink,
|
|
shareWith: '',
|
|
path: _ocsPath,
|
|
permissions: kPermissionRead,
|
|
password: password,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _addSharee() async {
|
|
final caps = context.read<NextcloudCapabilitiesCubit>();
|
|
final sharee = await AppRoutes.openShareePicker(
|
|
context,
|
|
allowUsers: caps.canShareWithUsers,
|
|
allowGroups: caps.canShareWithGroups,
|
|
);
|
|
if (sharee == null || !mounted) return;
|
|
await _runCreate(
|
|
FileSharingApiParams(
|
|
shareType: sharee.shareType,
|
|
shareWith: sharee.shareWith,
|
|
path: _ocsPath,
|
|
permissions: kPermissionRead,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final caps = context.watch<NextcloudCapabilitiesCubit>();
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (_busy) const LinearProgressIndicator(),
|
|
_shareList(),
|
|
const Divider(height: 1),
|
|
if (caps.canShareWithUsers || caps.canShareWithGroups)
|
|
ListTile(
|
|
leading: const CenteredLeading(Icon(Icons.person_add_outlined)),
|
|
title: const Text('Person oder Gruppe hinzufügen'),
|
|
enabled: !_busy,
|
|
onTap: _busy ? null : _addSharee,
|
|
),
|
|
if (caps.canCreatePublicLinks && _canAddLink(caps))
|
|
ListTile(
|
|
leading: const CenteredLeading(Icon(Icons.add_link)),
|
|
title: const Text('Öffentlichen Link erstellen'),
|
|
enabled: !_busy,
|
|
onTap: _busy ? null : _createPublicLink,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _openInTalk(Share share) {
|
|
final token = share.shareWith;
|
|
if (token == null || token.isEmpty) return;
|
|
AppRoutes.openChatByToken(context, token);
|
|
if (Navigator.of(context).canPop()) Navigator.of(context).pop();
|
|
}
|
|
|
|
/// Whether another public link may be created — hidden once one exists on a
|
|
/// server that disallows multiple links.
|
|
bool _canAddLink(NextcloudCapabilitiesCubit caps) {
|
|
if (caps.allowsMultipleLinks) return true;
|
|
final hasLink = _lastShares?.any((s) => s.isPublicLink) ?? false;
|
|
return !hasLink;
|
|
}
|
|
|
|
Widget _shareList() {
|
|
return FutureBuilder<List<Share>>(
|
|
future: _future,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(24),
|
|
child: Center(child: AppProgressIndicator.medium()),
|
|
);
|
|
}
|
|
if (snapshot.hasError) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
errorToUserMessage(snapshot.error),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 12),
|
|
FilledButton.tonal(
|
|
onPressed: _reload,
|
|
child: const Text('Erneut versuchen'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
final shares = snapshot.data ?? const <Share>[];
|
|
_lastShares = shares;
|
|
if (shares.isEmpty) {
|
|
final theme = Theme.of(context);
|
|
return SizedBox(
|
|
height: 160,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.lock_outline,
|
|
size: 48,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Diese Datei ist noch nicht freigegeben.',
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: shares.map(_shareTile).toList(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _shareTile(Share share) {
|
|
final preset = presetFromBitmask(share.permissions);
|
|
final subtitleParts = <String>[
|
|
// For links the URL already identifies the row; for everyone else show
|
|
// the kind so a Talk share isn't mistaken for a plain person share.
|
|
if (share.isPublicLink && share.url != null)
|
|
share.url!
|
|
else
|
|
share.kindLabel,
|
|
if (preset != null) preset.label,
|
|
if (share.hasPassword) 'passwortgeschützt',
|
|
if (share.expiration != null) 'bis ${share.expiration}',
|
|
];
|
|
return ListTile(
|
|
leading: CenteredLeading(Icon(shareIcon(share))),
|
|
title: Text(share.displayTitle),
|
|
subtitle: subtitleParts.isEmpty
|
|
? null
|
|
: Text(
|
|
subtitleParts.join(' · '),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (share.isPublicLink && share.url != null)
|
|
IconButton(
|
|
icon: const Icon(Icons.copy_outlined),
|
|
tooltip: 'Link kopieren',
|
|
onPressed: () => copyToClipboard(context, share.url!),
|
|
),
|
|
if (share.isRoom && (share.shareWith?.isNotEmpty ?? false))
|
|
IconButton(
|
|
icon: const Icon(Icons.chat_bubble_outline),
|
|
tooltip: 'Im Talk-Chat öffnen',
|
|
onPressed: () => _openInTalk(share),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.settings_outlined),
|
|
tooltip: 'Freigabe bearbeiten',
|
|
onPressed: () =>
|
|
showShareOptionsSheet(context, share, onChanged: _reload),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|