import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:result_monad/result_monad.dart'; import '../controls/linear_status_indicator.dart'; import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; import '../models/connection.dart'; import '../models/group_data.dart'; import '../routes.dart'; import '../services/connections_manager.dart'; import '../services/network_status_service.dart'; import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; class GroupEditorScreen extends StatefulWidget { final String groupId; const GroupEditorScreen({super.key, required this.groupId}); @override State createState() => _GroupEditorScreenState(); } class _GroupEditorScreenState extends State { final groupTextController = TextEditingController(); var processingUpdate = false; var allowNameEditing = false; var filterText = ''; late GroupData groupData; Future updateGroupName( BuildContext context, ConnectionsManager manager) async { processingUpdate = true; final updated = groupTextController.text; if (groupTextController.text != groupData.name) { final confirm = await showYesNoDialog( context, 'Change the group name from ${groupData.name} to $updated?'); if (context.mounted && confirm == true) { await manager.renameGroup(widget.groupId, updated).match( onSuccess: (updatedGroupData) { groupData = updatedGroupData; setState(() { allowNameEditing = false; }); }, onError: (error) { buildSnackbar(context, 'Error renaming group: $error'); }); } else { groupTextController.text = groupData.name; } } processingUpdate = false; } Future deleteGroup(ConnectionsManager manager) async { final confirm = await showYesNoDialog(context, "Permanently delete group ${groupData.name}? This can't be undone."); if (context.mounted && confirm == true) { await manager.deleteGroup(groupData).match( onSuccess: (_) => context.canPop() ? context.pop() : null, onError: (error) => buildSnackbar(context, 'Error trying to delete group: $error'), ); } } Future removeUserFromGroup( ConnectionsManager manager, Connection connection, ) async { final messageBase = '${connection.name} from ${groupData.name}'; final confirm = await showYesNoDialog(context, 'Remove $messageBase?'); if (context.mounted && confirm == true) { final message = await manager.removeUserFromGroup(groupData, connection).fold( onSuccess: (_) => 'Removed $messageBase', onError: (error) => 'Error removing $messageBase: $error', ); buildSnackbar(context, message); } } @override void initState() { super.initState(); final manager = getIt>().activeEntry.value; groupData = manager .getMyGroups() .where( (g) => g.id == widget.groupId, ) .first; manager.refreshGroupMemberships(groupData); groupTextController.text = groupData.name; } @override Widget build(BuildContext context) { final nss = getIt(); final manager = context .watch>() .activeEntry .value; final filterTextLC = filterText.toLowerCase(); final members = manager .getGroupMembers(groupData) .transform((ms) => ms .where((m) => filterText.isEmpty || m.name.toLowerCase().contains(filterTextLC) || m.handle.toLowerCase().contains(filterTextLC)) .toList()) .getValueOrElse(() => []); return Scaffold( appBar: StandardAppBar.build( context, 'Group Editor', withHome: false, actions: [ IconButton( onPressed: () => deleteGroup(manager), icon: const Icon(Icons.delete), ), ], ), body: Padding( padding: const EdgeInsets.all(8.0), child: RefreshIndicator( onRefresh: () async { manager.refreshGroups(); }, child: ResponsiveMaxWidth( child: Column( children: [ StandardLinearProgressIndicator(nss.connectionUpdateStatus), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextFormField( enabled: allowNameEditing, readOnly: !allowNameEditing, onEditingComplete: () async { if (processingUpdate) { return; } updateGroupName(context, manager); }, onTapOutside: (_) async { if (processingUpdate) { return; } updateGroupName(context, manager); }, controller: groupTextController, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: 'Group Name', border: OutlineInputBorder( borderSide: const BorderSide(), borderRadius: BorderRadius.circular(5.0), ), ), ), ), const HorizontalPadding(), IconButton( onPressed: () { if (allowNameEditing) { groupTextController.text = groupData.name; } setState(() { allowNameEditing = !allowNameEditing; }); }, icon: const Icon(Icons.edit), ), ], ), ), const VerticalPadding(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Group Members:', style: Theme.of(context).textTheme.headlineSmall), IconButton( onPressed: () { context.push( '${ScreenPaths.groupManagement}/add_users/${widget.groupId}'); }, icon: const Icon(Icons.add)), ], ), Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: TextField( onChanged: (value) { setState(() { filterText = value.toLowerCase(); }); }, decoration: InputDecoration( labelText: 'Filter By Name', alignLabelWithHint: true, border: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).highlightColor, ), borderRadius: BorderRadius.circular(5.0), ), ), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: StatusAndRefreshButton( valueListenable: nss.connectionUpdateStatus, refreshFunction: () async => manager.refreshGroupMemberships(groupData), ), ) ], ), Expanded( child: ListView.separated( physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { final m = members[index]; return ListTile( onTap: () { context.pushNamed(ScreenPaths.userProfile, pathParameters: {'id': m.id}); }, title: Text( '${m.name} (${m.handle})', softWrap: true, ), subtitle: Text( 'Last Status: ${m.lastStatus?.toIso8601String() ?? "Unknown"}', softWrap: true, ), trailing: IconButton( onPressed: () async => removeUserFromGroup(manager, m), icon: const Icon(Icons.remove), ), ); }, separatorBuilder: (_, __) => const Divider(), itemCount: members.length, ), ), ], ), ), ), )); } }