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/queries/share/share.dart'; import '../../../../api/marianumcloud/files_sharing/queries/share/share_update_params.dart'; import '../../../../api/marianumcloud/files_sharing/share_permissions.dart'; import '../../../../routing/app_routes.dart'; import '../../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart'; import '../../../../utils/clipboard_helper.dart'; import '../../../../widget/centered_leading.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/details_bottom_sheet.dart'; import '../../../../widget/info_dialog.dart'; import 'share_password_sheet.dart'; /// Two-digit zero-padded helper for building ISO `YYYY-MM-DD` strings (the /// format the OCS API expects for `expireDate`). String _two(int n) => n.toString().padLeft(2, '0'); String _isoDate(DateTime d) => '${d.year}-${_two(d.month)}-${_two(d.day)}'; IconData shareIcon(Share share) { if (share.isPublicLink) return Icons.link; if (share.isGroup) return Icons.groups_outlined; if (share.isRoom) return Icons.chat_bubble_outline; if (share.isEmail) return Icons.email_outlined; return Icons.person_outline; } /// Opens the edit/manage sheet for a single [share]. [onChanged] fires after /// every successful mutation so the parent share list can refresh. void showShareOptionsSheet( BuildContext context, Share share, { required VoidCallback onChanged, }) { showDetailsBottomSheet( context, header: ListTile( leading: CenteredLeading(Icon(shareIcon(share))), title: Text(share.displayTitle), subtitle: Text(share.kindLabel), ), children: (sheetCtx) => [ _ShareOptionsBody(share: share, onChanged: onChanged), ], ); } class _ShareOptionsBody extends StatefulWidget { final Share share; final VoidCallback onChanged; const _ShareOptionsBody({required this.share, required this.onChanged}); @override State<_ShareOptionsBody> createState() => _ShareOptionsBodyState(); } class _ShareOptionsBodyState extends State<_ShareOptionsBody> { late Share _share = widget.share; bool _busy = false; Future _mutate(Future Function() action) async { setState(() => _busy = true); try { final updated = await action(); if (!mounted) return; setState(() => _share = updated); widget.onChanged(); } catch (e) { if (mounted) { InfoDialog.show( context, errorToUserMessage(e), title: 'Fehler', copyable: true, ); } } finally { if (mounted) setState(() => _busy = false); } } List _availablePresets() => [ SharePreset.readOnly, SharePreset.edit, if (_share.isFolder) SharePreset.fileDrop, ]; Future _setPreset(SharePreset preset) async { final caps = context.read(); await _mutate( () => FileSharingApi().update( _share.id, ShareUpdateParams( permissions: permissionsFor( preset, allowReshare: caps.canReshare, isFolder: _share.isFolder, ), ), ), ); } Future _pickExpiry() async { final caps = context.read(); final now = DateTime.now(); final maxDays = caps.expireDays; final lastDate = maxDays != null && maxDays > 0 ? now.add(Duration(days: maxDays)) : now.add(const Duration(days: 365 * 5)); final picked = await showDatePicker( context: context, initialDate: now.add(const Duration(days: 7)), firstDate: now, lastDate: lastDate, ); if (picked == null) return; await _mutate( () => FileSharingApi().update( _share.id, ShareUpdateParams(expireDate: _isoDate(picked)), ), ); } Future _clearExpiry() => _mutate( () => FileSharingApi().update( _share.id, const ShareUpdateParams(expireDate: ''), ), ); Future _changePassword() async { final caps = context.read(); final value = await promptSharePassword( context, isChange: _share.hasPassword, policyHint: caps.passwordPolicyHint, ); if (value == null || value.isEmpty || !mounted) return; await _mutate( () => FileSharingApi().update( _share.id, ShareUpdateParams(password: value), ), ); } Future _clearPassword() => _mutate( () => FileSharingApi().update( _share.id, const ShareUpdateParams(password: ''), ), ); void _openInTalk() { final token = _share.shareWith; if (token == null || token.isEmpty) return; AppRoutes.openChatByToken(context, token); if (Navigator.of(context).canPop()) Navigator.of(context).pop(); } Future _confirmDelete() async { // Use showDialog directly (not asDialog) so we get the AsyncDialogAction's // `true` result and can close the options sheet afterwards — popping inside // onConfirmAsync would target the dialog route, not the sheet. final dialog = ConfirmDialog( title: 'Freigabe löschen?', content: 'Die Freigabe wird aufgehoben.', confirmButton: 'Löschen', onConfirmAsync: () => FileSharingApi().remove(_share.id), ); final deleted = await showDialog( context: context, builder: dialog.build, ); if (deleted != true || !mounted) return; widget.onChanged(); if (Navigator.of(context).canPop()) Navigator.of(context).pop(); } @override Widget build(BuildContext context) { final caps = context.watch(); final currentPreset = presetFromBitmask(_share.permissions); final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (_busy) const LinearProgressIndicator(), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text('Berechtigung', style: theme.textTheme.labelLarge), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap( spacing: 8, children: _availablePresets() .map( (p) => ChoiceChip( label: Text(p.label), selected: currentPreset == p, onSelected: _busy ? null : (_) => _setPreset(p), ), ) .toList(), ), ), if (_share.isRoom && (_share.shareWith?.isNotEmpty ?? false)) ...[ const Divider(height: 24), ListTile( leading: const CenteredLeading(Icon(Icons.chat_bubble_outline)), title: const Text('Im Talk-Chat öffnen'), onTap: _busy ? null : _openInTalk, ), ], if (_share.isPublicLink) ...[ const Divider(height: 24), if (_share.url != null) ListTile( leading: const CenteredLeading(Icon(Icons.link_outlined)), title: const Text('Öffentlicher Link'), subtitle: Text( _share.url!, maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: IconButton( onPressed: () => copyToClipboard(context, _share.url!), icon: const Icon(Icons.copy_outlined), ), ), ListTile( leading: const CenteredLeading(Icon(Icons.event_outlined)), title: const Text('Ablaufdatum'), subtitle: Text(_share.expiration ?? 'Kein Ablaufdatum'), trailing: _share.expiration != null && !caps.expireEnforced ? IconButton( icon: const Icon(Icons.clear), tooltip: 'Ablaufdatum entfernen', onPressed: _busy ? null : _clearExpiry, ) : null, onTap: _busy ? null : _pickExpiry, ), ListTile( leading: CenteredLeading( Icon( _share.hasPassword ? Icons.lock_outline : Icons.lock_open_outlined, ), ), title: Text( _share.hasPassword ? 'Passwortgeschützt' : 'Kein Passwort', ), subtitle: Text( caps.passwordEnforced && !_share.hasPassword ? 'Erforderlich – tippen zum Festlegen' : _share.hasPassword ? 'Tippen zum Ändern' : 'Tippen zum Festlegen', ), trailing: _share.hasPassword && !caps.passwordEnforced ? IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Passwort entfernen', onPressed: _busy ? null : _clearPassword, ) : null, onTap: _busy ? null : _changePassword, ), ], const Divider(height: 24), ListTile( leading: CenteredLeading( Icon(Icons.delete_outline, color: theme.colorScheme.error), ), title: Text( 'Freigabe löschen', style: TextStyle(color: theme.colorScheme.error), ), onTap: _busy ? null : _confirmDelete, ), ], ); } }