import 'package:flutter/material.dart'; import '../../../../api/errors/error_mapper.dart'; import '../../../../api/marianumcloud/autocomplete/autocomplete_api.dart'; import '../../../../api/marianumcloud/autocomplete/autocomplete_response.dart'; import '../../../../api/marianumcloud/files_sharing/queries/share/share.dart'; import '../../../../model/endpoint_data.dart'; import '../../../../utils/debouncer.dart'; import '../../../../utils/haptics.dart'; import '../../../../widget/app_progress_indicator.dart'; /// Result of [ShareePickerPage]: a recipient to create a share for. class ShareeRef { final int shareType; final String shareWith; final String label; const ShareeRef({ required this.shareType, required this.shareWith, required this.label, }); } /// Full-screen search for a user or group to share with. Which kinds are /// offered is gated by [allowUsers]/[allowGroups] (derived from the Nextcloud /// sharing capabilities at the call site). class ShareePickerPage extends StatefulWidget { final bool allowUsers; final bool allowGroups; const ShareePickerPage({ required this.allowUsers, required this.allowGroups, super.key, }); @override State createState() => _ShareePickerPageState(); } class _ShareePickerPageState extends State { final TextEditingController _searchController = TextEditingController(); static const String _debounceTag = 'sharee_picker_search'; String _query = ''; /// `null` = search every allowed kind at once. Only meaningful when both /// users and groups are allowed (otherwise the single allowed type is forced). int? _selectedShareType; Future? _future; @override void initState() { super.initState(); // When only one kind is allowed there is nothing to choose — force it. if (widget.allowUsers != widget.allowGroups) { _selectedShareType = widget.allowUsers ? kShareTypeUser : kShareTypeGroup; } } @override void dispose() { Debouncer.cancel(_debounceTag); _searchController.dispose(); super.dispose(); } List _effectiveShareTypes() { if (_selectedShareType != null) return [_selectedShareType!]; return [ if (widget.allowUsers) kShareTypeUser, if (widget.allowGroups) kShareTypeGroup, ]; } void _runSearch() { final query = _query; if (query.isEmpty) { setState(() => _future = null); return; } setState(() { _future = AutocompleteApi().find( query, shareTypes: _effectiveShareTypes(), ); }); } void _onQueryChanged(String value) { _query = value.trim(); Debouncer.debounce(_debounceTag, const Duration(milliseconds: 350), () { if (mounted) _runSearch(); }); } void _pick(AutocompleteResponseObject object) { Haptics.selection(); Navigator.of(context).pop( ShareeRef( shareType: shareTypeFromSource(object.source), shareWith: object.id, label: object.label, ), ); } @override Widget build(BuildContext context) { final showChips = widget.allowUsers && widget.allowGroups; return Scaffold( appBar: AppBar(title: const Text('Teilen mit…')), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: TextField( controller: _searchController, autofocus: true, onChanged: _onQueryChanged, decoration: InputDecoration( hintText: 'Name suchen…', prefixIcon: const Icon(Icons.search), suffixIcon: _query.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); _query = ''; _runSearch(); }, ), border: const OutlineInputBorder(), isDense: true, ), ), ), if (showChips) ...[ SizedBox(height: 40, child: _typeSelector()), const SizedBox(height: 8), ], Expanded(child: _results()), ], ), ); } Widget _typeSelector() => ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), children: [ _typeChip(null, 'Alle', Icons.apps), _typeChip(kShareTypeUser, 'Personen', Icons.person_outline), _typeChip(kShareTypeGroup, 'Gruppen', Icons.groups_outlined), ], ); Widget _typeChip(int? type, String label, IconData icon) => Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( avatar: Icon(icon, size: 18), showCheckmark: false, label: Text(label), selected: _selectedShareType == type, onSelected: (_) { Haptics.selection(); setState(() => _selectedShareType = type); _runSearch(); }, ), ); Widget _results() { if (_future == null) { return const Center(child: Text('Tippe, um nach Personen zu suchen.')); } return FutureBuilder( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: AppProgressIndicator.medium()); } if (snapshot.hasError) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( errorToUserMessage(snapshot.error), textAlign: TextAlign.center, ), ), ); } final results = snapshot.data?.data ?? const []; if (results.isEmpty) { return const Center(child: Text('Keine Treffer.')); } return ListView.builder( itemCount: results.length, itemBuilder: (context, index) => _resultTile(results[index]), ); }, ); } Widget _resultTile(AutocompleteResponseObject object) { final isGroup = shareTypeFromSource(object.source) == kShareTypeGroup; final leading = isGroup ? const CircleAvatar(child: Icon(Icons.groups_outlined)) : CircleAvatar( foregroundImage: Image.network( 'https://${EndpointData().nextcloud().full()}/avatar/${object.id}/128', ).image, backgroundColor: Theme.of(context).primaryColor, foregroundColor: Colors.white, child: const Icon(Icons.person), ); return ListTile( leading: leading, title: Text(object.label), subtitle: Text(isGroup ? 'Gruppe' : object.id), onTap: () => _pick(object), ); } }