import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.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/auth/profile.dart'; import '../models/connection.dart'; import '../models/timeline_grouping_list_data.dart'; import '../riverpod_controllers/account_services.dart'; import '../riverpod_controllers/circles_repo_services.dart'; import '../riverpod_controllers/networking/network_status_services.dart'; import '../routes.dart'; import '../utils/snackbar_builder.dart'; final _logger = Logger('CircleEditorScreen'); class CircleEditorScreen extends ConsumerStatefulWidget { final String circleId; const CircleEditorScreen({super.key, required this.circleId}); @override ConsumerState createState() => _CircleEditorScreenState(); } class _CircleEditorScreenState extends ConsumerState { final circleTextController = TextEditingController(); var processingUpdate = false; var allowNameEditing = false; var filterText = ''; late TimelineGroupingListData circleData; Future updateCircleName(BuildContext context, Profile profile) 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 ref .read(circlesProvider(profile).notifier) .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(Profile profile) async { final confirm = await showYesNoDialog(context, "Permanently delete circle ${circleData.name}? This can't be undone."); if (context.mounted && confirm == true) { await ref .read(circlesProvider(profile).notifier) .deleteCircle(circleData) .match( onSuccess: (_) => context.canPop() ? context.pop() : null, onError: (error) => buildSnackbar(context, 'Error trying to delete circle: $error'), ); } } Future removeUserFromCircle( Profile profile, 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 ref .read(circleMembersProvider(profile, circleData).notifier) .removeConnectionFromCircle(connection) .fold( onSuccess: (_) => 'Removed $messageBase', onError: (error) => 'Error removing $messageBase: $error', ); if (mounted) { buildSnackbar(context, message); } } } @override void initState() { _logger.fine('initState'); super.initState(); final profile = ref.read(activeProfileProvider); circleData = ref.read(circleDataProvider(profile, widget.circleId)).value; ref .read(circleMembersProvider(profile, circleData).notifier) .refreshCircleMemberships(); circleTextController.text = circleData.name; } @override Widget build(BuildContext context) { _logger.fine('build'); final filterTextLC = filterText.toLowerCase(); final profile = ref.watch(activeProfileProvider); final loading = ref.watch(groupMembersLoadingProvider(profile, circleData)); final members = ref .watch(circleMembersProvider(profile, circleData)) .where((m) => filterText.isEmpty || m.name.toLowerCase().contains(filterTextLC) || m.handle.toLowerCase().contains(filterTextLC)) .toList(); return Scaffold( appBar: StandardAppBar.build( context, 'Circle Editor', withHome: false, actions: [ IconButton( onPressed: () => deleteCircle(profile), icon: const Icon(Icons.delete), ), ], ), body: Padding( padding: const EdgeInsets.all(8.0), child: RefreshIndicator( onRefresh: () async { ref.read(circlesProvider(profile).notifier).refresh(); }, child: ResponsiveMaxWidth( child: Column( children: [ if (loading) const LinearProgressIndicator(), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextFormField( enabled: allowNameEditing, readOnly: !allowNameEditing, onEditingComplete: () async { if (processingUpdate) { return; } updateCircleName(context, profile); }, onTapOutside: (_) async { if (processingUpdate) { return; } updateCircleName(context, profile); }, 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( executing: loading, refreshFunction: () async => ref .read(circleMembersProvider(profile, circleData) .notifier) .refreshCircleMemberships(), ), ) ], ), 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(profile, m), icon: const Icon(Icons.remove), ), ); }, separatorBuilder: (_, __) => const Divider(), itemCount: members.length, ), ), ], ), ), ), )); } }