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/circle_data.dart'; import '../models/connection.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 CircleEditorScreen extends StatefulWidget { final String circleId; const CircleEditorScreen({super.key, required this.circleId}); @override State createState() => _CircleEditorScreenState(); } class _CircleEditorScreenState extends State { final circleTextController = TextEditingController(); var processingUpdate = false; var allowNameEditing = false; var filterText = ''; late CircleData circleData; Future updateCircleName( BuildContext context, ConnectionsManager manager) async { processingUpdate = true; final updated = circleTextController.text; if (circleTextController.text != circleData.name) { final confirm = await showYesNoDialog(context, 'Change the circle name from ${circleData.name} to $updated?'); if (context.mounted && confirm == true) { await manager.renameCircle(widget.circleId, updated).match( onSuccess: (updatedCircleData) { circleData = updatedCircleData; setState(() { allowNameEditing = false; }); }, onError: (error) { buildSnackbar(context, 'Error renaming circle: $error'); }); } else { circleTextController.text = circleData.name; } } processingUpdate = false; } Future deleteCircle(ConnectionsManager manager) async { final confirm = await showYesNoDialog(context, "Permanently delete circle ${circleData.name}? This can't be undone."); if (context.mounted && confirm == true) { await manager.deleteCircle(circleData).match( onSuccess: (_) => context.canPop() ? context.pop() : null, onError: (error) => buildSnackbar(context, 'Error trying to delete circle: $error'), ); } } Future removeUserFromCircle( ConnectionsManager manager, Connection connection, ) async { final messageBase = '${connection.name} from ${circleData.name}'; final confirm = await showYesNoDialog(context, 'Remove $messageBase?'); if (context.mounted && confirm == true) { final message = await manager.removeUserFromCircle(circleData, connection).fold( onSuccess: (_) => 'Removed $messageBase', onError: (error) => 'Error removing $messageBase: $error', ); buildSnackbar(context, message); } } @override void initState() { super.initState(); final manager = getIt>().activeEntry.value; circleData = manager .getMyCircles() .where( (g) => g.id == widget.circleId, ) .first; manager.refreshCircleMemberships(circleData); circleTextController.text = circleData.name; } @override Widget build(BuildContext context) { final nss = getIt(); final manager = context .watch>() .activeEntry .value; final filterTextLC = filterText.toLowerCase(); final members = manager .getCircleMembers(circleData) .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, 'Circle Editor', withHome: false, actions: [ IconButton( onPressed: () => deleteCircle(manager), icon: const Icon(Icons.delete), ), ], ), body: Padding( padding: const EdgeInsets.all(8.0), child: RefreshIndicator( onRefresh: () async { manager.refreshCircles(); }, 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; } updateCircleName(context, manager); }, onTapOutside: (_) async { if (processingUpdate) { return; } updateCircleName(context, manager); }, controller: circleTextController, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: 'Circle Name', border: OutlineInputBorder( borderSide: const BorderSide(), borderRadius: BorderRadius.circular(5.0), ), ), ), ), const HorizontalPadding(), IconButton( onPressed: () { if (allowNameEditing) { circleTextController.text = circleData.name; } setState(() { allowNameEditing = !allowNameEditing; }); }, icon: const Icon(Icons.edit), ), ], ), ), const VerticalPadding(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Circle Members (${members.length}):', style: Theme.of(context).textTheme.headlineSmall), IconButton( onPressed: () { context.push( '${ScreenPaths.circleManagement}/add_users/${widget.circleId}'); }, 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.refreshCircleMemberships(circleData), ), ) ], ), 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 => removeUserFromCircle(manager, m), icon: const Icon(Icons.remove), ), ); }, separatorBuilder: (_, __) => const Divider(), itemCount: members.length, ), ), ], ), ), ), )); } }