From 9e93faad2ef590b1422f87d3ae90b9537ed4f294 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Mon, 17 Apr 2023 16:37:48 -0400 Subject: [PATCH 01/20] Remove TODO for adding paging on getting group data since there is none --- lib/friendica_client/friendica_client.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 69890d7..bfe0607 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -154,7 +154,6 @@ class GroupsClient extends FriendicaClient { GroupsClient(super.credentials) : super(); - // TODO Convert Groups to using paging for real (if it is supported) FutureResult, ExecError> getGroups() async { _logger.finest(() => 'Getting group (Mastodon List) data'); final url = 'https://$serverName/api/v1/lists'; @@ -168,7 +167,6 @@ class GroupsClient extends FriendicaClient { : ExecError(type: ErrorType.localError, message: error.toString())); } - // TODO Convert groups for connection to using paging for real (if available) FutureResult, ExecError> getMemberGroupsForConnection( String connectionId) async { _logger.finest(() => From db7945ea77492edea8451f8814be80f907315477 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 18 Apr 2023 19:39:52 -0400 Subject: [PATCH 02/20] Add group management screen with group creation, renaming, and deleting --- lib/controls/standard_app_drawer.dart | 6 + lib/data/interfaces/groups_repo.intf.dart | 26 ++- lib/data/memory/memory_groups_repo.dart | 12 ++ lib/friendica_client/friendica_client.dart | 41 +++++ lib/models/group_data.dart | 18 +-- lib/routes.dart | 21 +++ lib/screens/group_create_screen.dart | 75 +++++++++ lib/screens/group_editor_screen.dart | 174 +++++++++++++++++++++ lib/screens/group_management_screen.dart | 58 +++++++ lib/services/connections_manager.dart | 35 +++++ pubspec.lock | 4 +- pubspec.yaml | 2 +- 12 files changed, 443 insertions(+), 29 deletions(-) create mode 100644 lib/screens/group_create_screen.dart create mode 100644 lib/screens/group_editor_screen.dart create mode 100644 lib/screens/group_management_screen.dart diff --git a/lib/controls/standard_app_drawer.dart b/lib/controls/standard_app_drawer.dart index 1d6a152..9d2d44b 100644 --- a/lib/controls/standard_app_drawer.dart +++ b/lib/controls/standard_app_drawer.dart @@ -73,6 +73,12 @@ class StandardAppDrawer extends StatelessWidget { 'Direct Messages', () => context.pushNamed(ScreenPaths.messages), ), + const Divider(), + buildMenuButton( + context, + 'Groups Management', + () => context.pushNamed(ScreenPaths.groupManagement), + ), buildMenuButton( context, 'Settings', diff --git a/lib/data/interfaces/groups_repo.intf.dart b/lib/data/interfaces/groups_repo.intf.dart index 55e66fa..cd09886 100644 --- a/lib/data/interfaces/groups_repo.intf.dart +++ b/lib/data/interfaces/groups_repo.intf.dart @@ -3,24 +3,18 @@ import 'package:result_monad/result_monad.dart'; import '../../models/exec_error.dart'; import '../../models/group_data.dart'; -class IGroupsRepo { - void addAllGroups(List groups) { - throw UnimplementedError(); - } +abstract class IGroupsRepo { + void addAllGroups(List groups); - void clearMyGroups() { - throw UnimplementedError(); - } + void clearMyGroups(); - List getMyGroups() { - throw UnimplementedError(); - } + void upsertGroup(GroupData group); - Result, ExecError> getGroupsForUser(String id) { - throw UnimplementedError(); - } + void deleteGroup(GroupData group); - bool updateConnectionGroupData(String id, List currentGroups) { - throw UnimplementedError(); - } + List getMyGroups(); + + Result, ExecError> getGroupsForUser(String id); + + bool updateConnectionGroupData(String id, List currentGroups); } diff --git a/lib/data/memory/memory_groups_repo.dart b/lib/data/memory/memory_groups_repo.dart index c44a0a0..a359703 100644 --- a/lib/data/memory/memory_groups_repo.dart +++ b/lib/data/memory/memory_groups_repo.dart @@ -40,4 +40,16 @@ class MemoryGroupsRepo implements IGroupsRepo { _groupsForConnection[id] = currentGroups; return true; } + + @override + void upsertGroup(GroupData group) { + _myGroups.remove(group); + _myGroups.add(group); + } + + @override + void deleteGroup(GroupData group) { + _groupsForConnection.remove(group.id); + _myGroups.remove(group); + } } diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index bfe0607..b724913 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -167,6 +167,47 @@ class GroupsClient extends FriendicaClient { : ExecError(type: ErrorType.localError, message: error.toString())); } + FutureResult createGroup(String title) async { + _logger.finest(() => 'Creating group (Mastodon List) of name $title'); + final url = 'https://$serverName/api/v1/lists'; + final body = { + 'title': title, + }; + final result = await postUrl( + Uri.parse(url), + body, + headers: _headers, + ).andThenSuccessAsync( + (data) async => GroupDataMastodonExtensions.fromJson(jsonDecode(data))); + return result.execErrorCast(); + } + + FutureResult renameGroup( + String id, String title) async { + _logger.finest(() => 'Reanming group (Mastodon List) to name $title'); + final url = 'https://$serverName/api/v1/lists/$id'; + final body = { + 'title': title, + }; + final result = await putUrl( + Uri.parse(url), + body, + headers: _headers, + ).andThenSuccessAsync((data) async { + final json = jsonDecode(data); + return GroupDataMastodonExtensions.fromJson(json); + }); + return result.execErrorCast(); + } + + FutureResult deleteGroup(GroupData groupData) async { + _logger.finest( + () => 'Reanming group (Mastodon List) to name ${groupData.name}'); + final url = 'https://$serverName/api/v1/lists/${groupData.id}'; + final result = await deleteUrl(Uri.parse(url), {}, headers: _headers); + return result.mapValue((_) => true).execErrorCast(); + } + FutureResult, ExecError> getMemberGroupsForConnection( String connectionId) async { _logger.finest(() => diff --git a/lib/models/group_data.dart b/lib/models/group_data.dart index 6c2c6a7..bd2c411 100644 --- a/lib/models/group_data.dart +++ b/lib/models/group_data.dart @@ -3,16 +3,6 @@ class GroupData { final String id; - @override - bool operator ==(Object other) => - identical(this, other) || - other is GroupData && - runtimeType == other.runtimeType && - id == other.id && - name == other.name; - - @override - int get hashCode => id.hashCode ^ name.hashCode; final String name; GroupData(this.id, this.name); @@ -21,4 +11,12 @@ class GroupData { String toString() { return 'GroupData{id: $id, name: $name}'; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GroupData && runtimeType == other.runtimeType && id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/lib/routes.dart b/lib/routes.dart index 6c0b115..f982e11 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -7,6 +7,9 @@ import 'screens/editor.dart'; import 'screens/follow_request_adjudication_screen.dart'; import 'screens/gallery_browsers_screen.dart'; import 'screens/gallery_screen.dart'; +import 'screens/group_create_screen.dart'; +import 'screens/group_editor_screen.dart'; +import 'screens/group_management_screen.dart'; import 'screens/home.dart'; import 'screens/interactions_viewer_screen.dart'; import 'screens/message_thread_screen.dart'; @@ -34,6 +37,7 @@ class ScreenPaths { static String notifications = '/notifications'; static String signin = '/signin'; static String manageProfiles = '/switchProfiles'; + static String groupManagement = '/group_management'; static String signup = '/signup'; static String userProfile = '/user_profile'; static String userPosts = '/user_posts'; @@ -120,6 +124,23 @@ final appRouter = GoRouter( builder: (context, state) => MessageThreadScreen(parentThreadId: state.queryParams['uri']!), ), + GoRoute( + name: ScreenPaths.groupManagement, + path: ScreenPaths.groupManagement, + builder: (context, state) => const GroupManagementScreen(), + routes: [ + GoRoute( + path: 'show/:id', + builder: (context, state) => GroupEditorScreen( + groupId: state.params['id']!, + ), + ), + GoRoute( + path: 'new', + builder: (context, state) => GroupCreateScreen(), + ), + ], + ), GoRoute( path: ScreenPaths.settings, name: ScreenPaths.settings, diff --git a/lib/screens/group_create_screen.dart b/lib/screens/group_create_screen.dart new file mode 100644 index 0000000..c5c522c --- /dev/null +++ b/lib/screens/group_create_screen.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../controls/padding.dart'; +import '../controls/standard_appbar.dart'; +import '../services/connections_manager.dart'; +import '../utils/active_profile_selector.dart'; +import '../utils/snackbar_builder.dart'; + +class GroupCreateScreen extends StatefulWidget { + GroupCreateScreen({super.key}); + + @override + State createState() => _GroupCreateScreenState(); +} + +class _GroupCreateScreenState extends State { + final groupTextController = TextEditingController(); + + Future createGroup(ConnectionsManager manager) async { + if (groupTextController.text.isEmpty) { + buildSnackbar(context, "Group name can't be empty"); + return; + } + + final result = await manager.createGroup(groupTextController.text); + if (context.mounted) { + result.match( + onSuccess: (_) => context.canPop() ? context.pop() : null, + onError: (error) => + buildSnackbar(context, 'Error trying to create new group: $error'), + ); + } + } + + @override + Widget build(BuildContext context) { + final manager = context + .watch>() + .activeEntry + .value; + + return Scaffold( + appBar: StandardAppBar.build( + context, + 'New group', + withHome: false, + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextFormField( + controller: groupTextController, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + labelText: 'Group Name', + border: OutlineInputBorder( + borderSide: const BorderSide(), + borderRadius: BorderRadius.circular(5.0), + ), + ), + ), + const VerticalPadding(), + ElevatedButton( + onPressed: () => createGroup(manager), + child: const Text('Create'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart new file mode 100644 index 0000000..f8dd128 --- /dev/null +++ b/lib/screens/group_editor_screen.dart @@ -0,0 +1,174 @@ +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/padding.dart'; +import '../controls/responsive_max_width.dart'; +import '../controls/standard_appbar.dart'; +import '../globals.dart'; +import '../models/group_data.dart'; +import '../services/connections_manager.dart'; +import '../utils/active_profile_selector.dart'; +import '../utils/snackbar_builder.dart'; + +class GroupEditorScreen extends StatefulWidget { + final String groupId; + + GroupEditorScreen({super.key, required this.groupId}); + + @override + State createState() => _GroupEditorScreenState(); +} + +class _GroupEditorScreenState extends State { + final groupTextController = TextEditingController(); + var processingUpdate = false; + var allowNameEditing = false; + 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) { + if (mounted) { + 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'), + ); + } + } + + @override + void initState() { + super.initState(); + final manager = + getIt>().activeEntry.value; + groupData = manager + .getMyGroups() + .where( + (g) => g.id == widget.groupId, + ) + .first; + groupTextController.text = groupData.name; + } + + @override + Widget build(BuildContext context) { + final manager = context + .watch>() + .activeEntry + .value; + + 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: [ + 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), + ), + ], + ), + ), + Expanded( + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + return ListTile( + title: Text("User"), + ); + }, + separatorBuilder: (_, __) => const Divider(), + itemCount: 1, + ), + ), + ], + ), + ), + ), + )); + } +} diff --git a/lib/screens/group_management_screen.dart b/lib/screens/group_management_screen.dart new file mode 100644 index 0000000..71fd9a0 --- /dev/null +++ b/lib/screens/group_management_screen.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import '../controls/responsive_max_width.dart'; +import '../controls/standard_appbar.dart'; +import '../routes.dart'; +import '../services/connections_manager.dart'; +import '../utils/active_profile_selector.dart'; + +class GroupManagementScreen extends StatelessWidget { + const GroupManagementScreen({super.key}); + + @override + Widget build(BuildContext context) { + final manager = context + .watch>() + .activeEntry + .value; + final groups = manager.getMyGroups(); + return Scaffold( + appBar: StandardAppBar.build( + context, + 'Groups Management', + withHome: false, + actions: [ + IconButton( + onPressed: () => context.push( + '${ScreenPaths.groupManagement}/new', + ), + icon: const Icon(Icons.add), + ), + ], + ), + body: Center( + child: RefreshIndicator( + onRefresh: () async { + manager.refreshGroups(); + }, + child: ResponsiveMaxWidth( + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + final group = groups[index]; + return ListTile( + title: Text(group.name), + onTap: () => context.push( + '${ScreenPaths.groupManagement}/show/${group.id}'), + ); + }, + separatorBuilder: (_, __) => const Divider(), + itemCount: groups.length, + ), + ), + ), + )); + } +} diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 737eff2..5afc74f 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -194,6 +194,41 @@ class ConnectionsManager extends ChangeNotifier { return myGroups; } + FutureResult createGroup(String newName) async { + final result = await GroupsClient(getIt().currentProfile) + .createGroup(newName) + .withResultAsync((newGroup) async { + groupsRepo.upsertGroup(newGroup); + notifyListeners(); + }); + return result.execErrorCast(); + } + + FutureResult renameGroup( + String id, String newName) async { + final result = await GroupsClient(getIt().currentProfile) + .renameGroup(id, newName) + .withResultAsync((renamedGroup) async { + groupsRepo.upsertGroup(renamedGroup); + notifyListeners(); + }); + return result.execErrorCast(); + } + + FutureResult deleteGroup(GroupData groupData) async { + final result = await GroupsClient(getIt().currentProfile) + .deleteGroup(groupData) + .withResultAsync((_) async { + groupsRepo.deleteGroup(groupData); + notifyListeners(); + }); + return result.execErrorCast(); + } + + void refreshGroups() { + _updateMyGroups(true); + } + Result, ExecError> getGroupsForUser(String id) { final result = groupsRepo.getGroupsForUser(id); if (result.isSuccess) { diff --git a/pubspec.lock b/pubspec.lock index 11979e7..669ba60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -948,10 +948,10 @@ packages: dependency: "direct main" description: name: result_monad - sha256: "8f7720b9d517dbb54d612e2f6c6c4f409d51374f0d9ff9749dfcb0e0c6ab2fd4" + sha256: "59e65e969f93c8aff18104f36233b0fd102a096d6501d3515e2a80cd67f3565a" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c89d0c0..98dcc01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: network_to_file_image: ^4.0.1 path: ^1.8.2 provider: ^6.0.4 - result_monad: ^2.0.2 + result_monad: ^2.1.0 scrollable_positioned_list: ^0.3.5 shared_preferences: ^2.0.15 sqlite3: ^1.9.1 From 8377f640b6e0d429eb41297f8eed2866248375b1 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 18 Apr 2023 20:33:09 -0400 Subject: [PATCH 03/20] Fix disappearing Follows issue caused by partially populated account data in some endpoints --- .../interfaces/connections_repo_intf.dart | 38 ++++--------------- lib/data/memory/memory_connections_repo.dart | 21 +--------- .../objectbox/objectbox_connections_repo.dart | 23 ++--------- lib/screens/contacts_screen.dart | 14 +++++-- lib/screens/group_editor_screen.dart | 3 ++ .../direct_message_friendica_extensions.dart | 8 +--- .../notification_mastodon_extension.dart | 2 +- .../timeline_entry_mastodon_extensions.dart | 4 +- lib/services/connections_manager.dart | 32 +++++++++------- 9 files changed, 50 insertions(+), 95 deletions(-) diff --git a/lib/data/interfaces/connections_repo_intf.dart b/lib/data/interfaces/connections_repo_intf.dart index 158d0da..a358c26 100644 --- a/lib/data/interfaces/connections_repo_intf.dart +++ b/lib/data/interfaces/connections_repo_intf.dart @@ -3,40 +3,18 @@ import 'package:result_monad/result_monad.dart'; import '../../models/connection.dart'; import '../../models/exec_error.dart'; -class IConnectionsRepo { - void clear() { - throw UnimplementedError(); - } +abstract class IConnectionsRepo { + void clear(); - bool addConnection(Connection connection) { - throw UnimplementedError(); - } + bool upsertConnection(Connection connection); - bool addAllConnections(Iterable newConnections) { - throw UnimplementedError(); - } + Result getById(String id); - bool updateConnection(Connection connection) { - throw UnimplementedError(); - } + Result getByName(String name); - Result getById(String id) { - throw UnimplementedError(); - } + Result getByHandle(String handle); - Result getByName(String name) { - throw UnimplementedError(); - } + List getMyContacts(); - Result getByHandle(String handle) { - throw UnimplementedError(); - } - - List getMyContacts() { - throw UnimplementedError(); - } - - List getKnownUsersByName(String name) { - throw UnimplementedError(); - } + List getKnownUsersByName(String name); } diff --git a/lib/data/memory/memory_connections_repo.dart b/lib/data/memory/memory_connections_repo.dart index 563441f..861b244 100644 --- a/lib/data/memory/memory_connections_repo.dart +++ b/lib/data/memory/memory_connections_repo.dart @@ -16,25 +16,6 @@ class MemoryConnectionsRepo implements IConnectionsRepo { _myContacts.clear(); } - @override - bool addAllConnections(Iterable newConnections) { - bool result = true; - - for (final connection in newConnections) { - result &= addConnection(connection); - } - - return result; - } - - @override - bool addConnection(Connection connection) { - if (_connectionsById.containsKey(connection.id)) { - return false; - } - return updateConnection(connection); - } - @override List getKnownUsersByName(String name) { return _connectionsByName.values.where((it) { @@ -47,7 +28,7 @@ class MemoryConnectionsRepo implements IConnectionsRepo { } @override - bool updateConnection(Connection connection) { + bool upsertConnection(Connection connection) { _connectionsById[connection.id] = connection; _connectionsByName[connection.name] = connection; int index = _myContacts.indexWhere((c) => c.id == connection.id); diff --git a/lib/data/objectbox/objectbox_connections_repo.dart b/lib/data/objectbox/objectbox_connections_repo.dart index f9e40e1..186bf29 100644 --- a/lib/data/objectbox/objectbox_connections_repo.dart +++ b/lib/data/objectbox/objectbox_connections_repo.dart @@ -21,23 +21,6 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo { box.removeAll(); } - @override - bool addAllConnections(Iterable newConnections) { - var allNew = true; - for (final c in newConnections) { - allNew &= addConnection(c); - } - return allNew; - } - - @override - bool addConnection(Connection connection) { - if (memCache.addConnection(connection)) { - box.putAsync(connection); - } - return true; - } - @override Result getById(String id) { final result = memCache.getById(id); @@ -109,8 +92,8 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo { } @override - bool updateConnection(Connection connection) { - memCache.updateConnection(connection); + bool upsertConnection(Connection connection) { + memCache.upsertConnection(connection); box.put(connection); return true; } @@ -121,7 +104,7 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo { return buildErrorResult(type: ErrorType.notFound); } - memCache.addConnection(connection); + memCache.upsertConnection(connection); return Result.ok(connection); } } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 2cfd417..69293a3 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -8,6 +8,7 @@ import '../controls/current_profile_button.dart'; import '../controls/linear_status_indicator.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_app_drawer.dart'; +import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; import '../models/connection.dart'; import '../routes.dart'; @@ -47,7 +48,7 @@ class _ContactsScreenState extends State { ); late Widget body; if (contacts.isEmpty) { - body = SingleChildScrollView( + body = const SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), child: Center( child: Text('No contacts'), @@ -73,7 +74,7 @@ class _ContactsScreenState extends State { } return Scaffold( - drawer: StandardAppDrawer(skipPopDismiss: true), + drawer: const StandardAppDrawer(skipPopDismiss: true), body: SafeArea( child: RefreshIndicator( onRefresh: () async { @@ -103,13 +104,20 @@ class _ContactsScreenState extends State { alignLabelWithHint: true, border: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).backgroundColor, + 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.updateAllContacts(), + ), ) ], ), diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart index f8dd128..12c0372 100644 --- a/lib/screens/group_editor_screen.dart +++ b/lib/screens/group_editor_screen.dart @@ -153,6 +153,9 @@ class _GroupEditorScreenState extends State { ], ), ), + const VerticalPadding(), + ElevatedButton( + onPressed: () {}, child: const Text('Add Users')), Expanded( child: ListView.separated( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/serializers/friendica/direct_message_friendica_extensions.dart b/lib/serializers/friendica/direct_message_friendica_extensions.dart index 830d41c..f66be37 100644 --- a/lib/serializers/friendica/direct_message_friendica_extensions.dart +++ b/lib/serializers/friendica/direct_message_friendica_extensions.dart @@ -46,16 +46,12 @@ extension DirectMessageFriendicaExtension on DirectMessage { .andThenSuccess((cm) { if (getIt().currentProfile.userId != senderId) { final s = ConnectionFriendicaExtensions.fromJson(json['sender']); - if (cm.getById(s.id).isFailure) { - cm.addConnection(s); - } + cm.upsertConnection(s); } if (getIt().currentProfile.userId != recipientId) { final r = ConnectionFriendicaExtensions.fromJson(json['recipient']); - if (cm.getById(r.id).isFailure) { - cm.addConnection(r); - } + cm.upsertConnection(r); } }); diff --git a/lib/serializers/mastodon/notification_mastodon_extension.dart b/lib/serializers/mastodon/notification_mastodon_extension.dart index fc880d5..06fbc40 100644 --- a/lib/serializers/mastodon/notification_mastodon_extension.dart +++ b/lib/serializers/mastodon/notification_mastodon_extension.dart @@ -28,7 +28,7 @@ extension NotificationMastodonExtension on UserNotification { final from = ConnectionMastodonExtensions.fromJson(json['account']); getIt>() .activeEntry - .andThenSuccess((manager) => manager.addConnection(from)); + .andThenSuccess((manager) => manager.upsertConnection(from)); var statusId = ''; var statusLink = ''; var content = ''; diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 7111a08..0d6c235 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -72,14 +72,14 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { return null; }); final connection = ConnectionMastodonExtensions.fromJson(json['account']); - connectionManager?.addConnection(connection); + connectionManager?.upsertConnection(connection); late final String reshareAuthor; late final String reshareAuthorId; if (json['reblog'] != null) { final rebloggedUser = ConnectionMastodonExtensions.fromJson(json['reblog']['account']); - connectionManager?.addConnection(rebloggedUser); + connectionManager?.upsertConnection(rebloggedUser); reshareAuthor = rebloggedUser.name; reshareAuthorId = rebloggedUser.id; } else { diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 5afc74f..f5210fe 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -22,20 +22,26 @@ class ConnectionsManager extends ChangeNotifier { ConnectionsManager(this.conRepo, this.groupsRepo); - bool addConnection(Connection connection) { - return conRepo.addConnection(connection); - } - List getKnownUsersByName(String name) { return conRepo.getKnownUsersByName(name); } - bool updateConnection(Connection connection) { - return conRepo.updateConnection(connection); + bool upsertConnection(Connection connection) { + if (connection.status != ConnectionStatus.unknown) { + return conRepo.upsertConnection(connection); + } + + return conRepo.getById(connection.id).fold( + onSuccess: (original) => conRepo.upsertConnection( + connection.copy(status: original.status), + ), + onError: (_) => conRepo.upsertConnection(connection), + ); } bool addAllConnections(Iterable newConnections) { - return conRepo.addAllConnections(newConnections); + newConnections.forEach(upsertConnection); + return true; } Future acceptFollowRequest(Connection connection) async { @@ -47,7 +53,7 @@ class ConnectionsManager extends ChangeNotifier { onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); - updateConnection(update); + upsertConnection(update); notifyListeners(); }, onError: (error) { @@ -65,7 +71,7 @@ class ConnectionsManager extends ChangeNotifier { onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); - updateConnection(update); + upsertConnection(update); notifyListeners(); }, onError: (error) { @@ -83,7 +89,7 @@ class ConnectionsManager extends ChangeNotifier { onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); - updateConnection(update); + upsertConnection(update); notifyListeners(); }, onError: (error) { @@ -101,7 +107,7 @@ class ConnectionsManager extends ChangeNotifier { onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); - updateConnection(update); + upsertConnection(update); notifyListeners(); }, onError: (error) { @@ -119,7 +125,7 @@ class ConnectionsManager extends ChangeNotifier { onSuccess: (update) { _logger .finest('Successfully unfollowed ${update.name}: ${update.status}'); - updateConnection(update); + upsertConnection(update); notifyListeners(); }, onError: (error) { @@ -333,7 +339,7 @@ class ConnectionsManager extends ChangeNotifier { .getConnectionWithStatus(connection) .match( onSuccess: (update) { - updateConnection(update); + upsertConnection(update); if (withNotification) { notifyListeners(); } From 2423dfbddf52cddddac8d103ee30ef909a4d83cf Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 18 Apr 2023 20:59:51 -0400 Subject: [PATCH 04/20] Fix update followed/following prunes no longer followed people initiated from outside app --- lib/services/connections_manager.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index f5210fe..50da303 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -39,7 +39,7 @@ class ConnectionsManager extends ChangeNotifier { ); } - bool addAllConnections(Iterable newConnections) { + bool upsertAllConnections(Iterable newConnections) { newConnections.forEach(upsertConnection); return true; } @@ -144,11 +144,13 @@ class ConnectionsManager extends ChangeNotifier { final results = {}; var moreResults = true; var maxId = -1; - const limit = 200; + const limit = 50; var currentPage = PagingData(limit: limit); + final originalContacts = conRepo.getMyContacts().toSet(); while (moreResults) { await client.getMyFollowers(currentPage).match(onSuccess: (followers) { for (final f in followers.data) { + originalContacts.remove(f); results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou); int id = int.parse(f.id); maxId = max(maxId, id); @@ -167,6 +169,7 @@ class ConnectionsManager extends ChangeNotifier { while (moreResults) { await client.getMyFollowing(currentPage).match(onSuccess: (following) { for (final f in following.data) { + originalContacts.remove(f); if (results.containsKey(f.id)) { results[f.id] = f.copy(status: ConnectionStatus.mutual); } else { @@ -184,7 +187,11 @@ class ConnectionsManager extends ChangeNotifier { }); } - addAllConnections(results.values); + for (final noLongerFollowed in originalContacts) { + results[noLongerFollowed.id] = + noLongerFollowed.copy(status: ConnectionStatus.none); + } + upsertAllConnections(results.values); final myContacts = conRepo.getMyContacts().toList(); myContacts.sort((c1, c2) => c1.name.compareTo(c2.name)); _logger.fine('# Contacts:${myContacts.length}'); From 35ffbd7273834c68f9ec6e01ba606ac94a2e12a2 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 18 Apr 2023 21:00:20 -0400 Subject: [PATCH 05/20] Fix ObjectBox Connection upsert keying off DB ID not Fediverse ID --- lib/data/objectbox/objectbox_connections_repo.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/data/objectbox/objectbox_connections_repo.dart b/lib/data/objectbox/objectbox_connections_repo.dart index 186bf29..37898ab 100644 --- a/lib/data/objectbox/objectbox_connections_repo.dart +++ b/lib/data/objectbox/objectbox_connections_repo.dart @@ -94,7 +94,10 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo { @override bool upsertConnection(Connection connection) { memCache.upsertConnection(connection); - box.put(connection); + getById(connection.id).match( + onSuccess: (existing) => box.put(connection.copy(obId: existing.obId)), + onError: (_) => box.put(connection), + ); return true; } From e28cf85b463972b6828651fa823064d6f2715a93 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 18 Apr 2023 21:49:45 -0400 Subject: [PATCH 06/20] Group Editor now lists users in group (needs polishing though) --- lib/data/interfaces/groups_repo.intf.dart | 5 + lib/data/memory/memory_groups_repo.dart | 25 +++- lib/friendica_client/friendica_client.dart | 19 +++ lib/screens/group_editor_screen.dart | 129 +++++++++++---------- lib/screens/group_management_screen.dart | 1 + lib/services/connections_manager.dart | 30 +++++ 6 files changed, 146 insertions(+), 63 deletions(-) diff --git a/lib/data/interfaces/groups_repo.intf.dart b/lib/data/interfaces/groups_repo.intf.dart index cd09886..c4ceec3 100644 --- a/lib/data/interfaces/groups_repo.intf.dart +++ b/lib/data/interfaces/groups_repo.intf.dart @@ -1,11 +1,14 @@ import 'package:result_monad/result_monad.dart'; +import '../../models/connection.dart'; import '../../models/exec_error.dart'; import '../../models/group_data.dart'; abstract class IGroupsRepo { void addAllGroups(List groups); + void addConnectionToGroup(GroupData group, Connection connection); + void clearMyGroups(); void upsertGroup(GroupData group); @@ -14,6 +17,8 @@ abstract class IGroupsRepo { List getMyGroups(); + Result, ExecError> getGroupMembers(GroupData group); + Result, ExecError> getGroupsForUser(String id); bool updateConnectionGroupData(String id, List currentGroups); diff --git a/lib/data/memory/memory_groups_repo.dart b/lib/data/memory/memory_groups_repo.dart index a359703..2251754 100644 --- a/lib/data/memory/memory_groups_repo.dart +++ b/lib/data/memory/memory_groups_repo.dart @@ -1,11 +1,13 @@ import 'package:result_monad/result_monad.dart'; +import '../../models/connection.dart'; import '../../models/exec_error.dart'; import '../../models/group_data.dart'; import '../interfaces/groups_repo.intf.dart'; class MemoryGroupsRepo implements IGroupsRepo { final _groupsForConnection = >{}; + final _connectionsForGroup = >{}; final _myGroups = {}; @override @@ -25,11 +27,28 @@ class MemoryGroupsRepo implements IGroupsRepo { return _myGroups.toList(); } + @override + Result, ExecError> getGroupMembers(GroupData group) { + if (_connectionsForGroup.containsKey(group.id)) { + return Result.ok(_connectionsForGroup[group.id]!.toList()); + } + + return buildErrorResult( + type: ErrorType.notFound, + message: 'Group ${group.id} not found', + ); + } + @override void clearMyGroups() { _myGroups.clear(); } + @override + void addConnectionToGroup(GroupData group, Connection connection) { + _connectionsForGroup.putIfAbsent(group.id, () => {}).add(connection); + } + @override void addAllGroups(List groups) { _myGroups.addAll(groups); @@ -43,13 +62,17 @@ class MemoryGroupsRepo implements IGroupsRepo { @override void upsertGroup(GroupData group) { + _connectionsForGroup.putIfAbsent(group.id, () => {}); _myGroups.remove(group); _myGroups.add(group); } @override void deleteGroup(GroupData group) { - _groupsForConnection.remove(group.id); + for (final conGroups in _groupsForConnection.values) { + conGroups.remove(group); + } + _connectionsForGroup.remove(group.id); _myGroups.remove(group); } } diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index b724913..f35d1ef 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -167,6 +167,25 @@ class GroupsClient extends FriendicaClient { : ExecError(type: ErrorType.localError, message: error.toString())); } + FutureResult>, ExecError> getGroupMembers( + GroupData groupData, + PagingData page, + ) async { + _networkStatusService.startConnectionUpdateStatus(); + _logger.finest(() => + 'Getting members for group (Mastodon List) of name ${groupData.name} with paging: $page'); + final baseUrl = 'https://$serverName/api/v1/lists/${groupData.id}/accounts'; + final url = Uri.parse('$baseUrl?${page.toQueryParameters()}'); + final result = await _getApiPagedRequest(url); + _networkStatusService.finishConnectionUpdateStatus(); + return result + .andThenSuccess((response) => response.map((jsonArray) => + (jsonArray as List) + .map((json) => ConnectionMastodonExtensions.fromJson(json)) + .toList())) + .execErrorCast(); + } + FutureResult createGroup(String title) async { _logger.finest(() => 'Creating group (Mastodon List) of name $title'); final url = 'https://$serverName/api/v1/lists'; diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart index 12c0372..69a4adf 100644 --- a/lib/screens/group_editor_screen.dart +++ b/lib/screens/group_editor_screen.dart @@ -3,12 +3,13 @@ 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 '../globals.dart'; import '../models/group_data.dart'; import '../services/connections_manager.dart'; +import '../services/network_status_service.dart'; import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; @@ -76,16 +77,20 @@ class _GroupEditorScreenState extends State { (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 members = manager.getGroupMembers(groupData).getValueOrElse(() => []); + return Scaffold( appBar: StandardAppBar.build( context, @@ -104,72 +109,72 @@ class _GroupEditorScreenState extends State { onRefresh: () async { manager.refreshGroups(); }, - child: ResponsiveMaxWidth( - child: Column( - children: [ - 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), - ), + 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 HorizontalPadding(), + IconButton( + onPressed: () { + if (allowNameEditing) { + groupTextController.text = groupData.name; + } + setState(() { + allowNameEditing = !allowNameEditing; + }); + }, + icon: const Icon(Icons.edit), + ), + ], ), - const VerticalPadding(), - ElevatedButton( - onPressed: () {}, child: const Text('Add Users')), - Expanded( - child: ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) { - return ListTile( - title: Text("User"), - ); - }, - separatorBuilder: (_, __) => const Divider(), - itemCount: 1, - ), + ), + const VerticalPadding(), + ElevatedButton( + onPressed: () {}, child: const Text('Add Users')), + Expanded( + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + final m = members[index]; + return ListTile( + title: Text(m.name), + ); + }, + separatorBuilder: (_, __) => const Divider(), + itemCount: members.length, ), - ], - ), + ), + ], ), ), )); diff --git a/lib/screens/group_management_screen.dart b/lib/screens/group_management_screen.dart index 71fd9a0..ab6fe4b 100644 --- a/lib/screens/group_management_screen.dart +++ b/lib/screens/group_management_screen.dart @@ -18,6 +18,7 @@ class GroupManagementScreen extends StatelessWidget { .activeEntry .value; final groups = manager.getMyGroups(); + groups.sort((g1, g2) => g1.name.compareTo(g2.name)); return Scaffold( appBar: StandardAppBar.build( context, diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 50da303..b2c3409 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -207,6 +207,10 @@ class ConnectionsManager extends ChangeNotifier { return myGroups; } + Result, ExecError> getGroupMembers(GroupData group) { + return groupsRepo.getGroupMembers(group); + } + FutureResult createGroup(String newName) async { final result = await GroupsClient(getIt().currentProfile) .createGroup(newName) @@ -242,6 +246,32 @@ class ConnectionsManager extends ChangeNotifier { _updateMyGroups(true); } + Future refreshGroupMemberships(GroupData group) async { + var page = PagingData(limit: 50); + final client = GroupsClient(getIt().currentProfile); + final allResults = {}; + var moreResults = true; + while (moreResults) { + await client.getGroupMembers(group, page).match(onSuccess: (results) { + moreResults = results.data.isNotEmpty && results.next != null; + page = results.next ?? page; + allResults.addAll(results.data); + }, onError: (error) { + _logger.severe('Error getting group listing data: $error'); + moreResults = false; + }); + } + + groupsRepo.deleteGroup(group); + groupsRepo.upsertGroup(group); + for (final c in allResults) { + upsertConnection(c); + groupsRepo.addConnectionToGroup(group, c); + } + + notifyListeners(); + } + Result, ExecError> getGroupsForUser(String id) { final result = groupsRepo.getGroupsForUser(id); if (result.isSuccess) { From aa0fd14c5a2a2070fedbb893ed6945f0e2763216 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 08:34:33 -0400 Subject: [PATCH 07/20] Add device_preview to the app for better previewing capabilities --- lib/main.dart | 11 ++++++++++- pubspec.lock | 37 +++++++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index b00f87c..3b50dd9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'package:device_preview/device_preview.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; @@ -25,6 +27,7 @@ import 'utils/old_android_letsencrypte_cert.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // await dotenv.load(fileName: '.env'); + const enablePreview = false; Logger.root.level = Level.FINER; Logger.root.onRecord.listen((event) { final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; @@ -36,7 +39,10 @@ void main() async { await fixLetsEncryptCertOnOldAndroid(); await dependencyInjectionInitialization(); - runApp(const App()); + runApp(DevicePreview( + enabled: !kReleaseMode && enablePreview, + builder: (context) => const App(), + )); } class App extends StatelessWidget { @@ -102,6 +108,9 @@ class App extends StatelessWidget { ) ], child: MaterialApp.router( + useInheritedMediaQuery: true, + locale: DevicePreview.locale(context), + builder: DevicePreview.appBuilder, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: getIt().themeMode, diff --git a/pubspec.lock b/pubspec.lock index 669ba60..cdfd75b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + device_frame: + dependency: transitive + description: + name: device_frame + sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d + url: "https://pub.dev" + source: hosted + version: "1.1.0" device_info_plus: dependency: "direct main" description: @@ -265,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + device_preview: + dependency: "direct main" + description: + name: device_preview + sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" + url: "https://pub.dev" + source: hosted + version: "1.1.0" fake_async: dependency: transitive description: @@ -358,6 +374,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -456,6 +477,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" + source: hosted + version: "2.2.0" frontend_server_client: dependency: transitive description: @@ -600,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.3" + intl: + dependency: transitive + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 98dcc01..03b4995 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: carousel_slider: ^4.2.1 device_info_plus: ^8.0.0 string_validator: ^0.3.0 + device_preview: ^1.1.0 dev_dependencies: flutter_test: From c431afffc313b9cf0c150ecdbd1e94fb4d6c86f8 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 09:46:29 -0400 Subject: [PATCH 08/20] Add color blindness testing capabilities in non-release mode --- lib/app_theme.dart | 38 ++++-- .../audio_video/media_kit_av_control.dart | 2 +- lib/main.dart | 15 ++- lib/screens/settings_screen.dart | 17 +++ lib/services/setting_service.dart | 24 ++++ pubspec.lock | 120 ++++++++++-------- pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 2 +- 9 files changed, 152 insertions(+), 70 deletions(-) diff --git a/lib/app_theme.dart b/lib/app_theme.dart index 47bdaee..97957db 100644 --- a/lib/app_theme.dart +++ b/lib/app_theme.dart @@ -1,14 +1,36 @@ +import 'package:color_blindness/color_blindness.dart'; +import 'package:color_blindness/color_blindness_color_scheme.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -class AppTheme { - static ThemeData light = ThemeData( - colorSchemeSeed: Colors.indigo, - useMaterial3: true, - ); +const _seedColor = Colors.indigo; +final _lightScheme = ColorScheme.fromSeed( + seedColor: _seedColor, + brightness: Brightness.light, +); - static ThemeData dark = ThemeData( - colorSchemeSeed: Colors.indigo, +final _darkScheme = ColorScheme.fromSeed( + seedColor: _seedColor, + brightness: Brightness.dark, +); + +ThemeData buildTheme({ + required Brightness brightness, + ColorBlindnessType blindnessType = ColorBlindnessType.none, +}) { + final baseScheme = + brightness == Brightness.light ? _lightScheme : _darkScheme; + late final ColorScheme scheme; + + if (!kReleaseMode && blindnessType != ColorBlindnessType.none) { + scheme = colorBlindnessColorScheme(baseScheme, blindnessType); + } else { + scheme = baseScheme; + } + + return ThemeData( + colorScheme: scheme, + brightness: brightness, useMaterial3: true, - brightness: Brightness.dark, ); } diff --git a/lib/controls/audio_video/media_kit_av_control.dart b/lib/controls/audio_video/media_kit_av_control.dart index 4d0d557..305ccf3 100644 --- a/lib/controls/audio_video/media_kit_av_control.dart +++ b/lib/controls/audio_video/media_kit_av_control.dart @@ -30,7 +30,7 @@ class _MediaKitAvControlState extends State { super.initState(); Future.microtask(() async { _logger.info('initializing'); - controller = await VideoController.create(player.handle); + controller = await VideoController.create(player); _logger.info('initialized'); if (context.mounted) { setState(() {}); diff --git a/lib/main.dart b/lib/main.dart index 3b50dd9..b6b2e32 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,7 @@ import 'utils/old_android_letsencrypte_cert.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // await dotenv.load(fileName: '.env'); - const enablePreview = false; + const enablePreview = true; Logger.root.level = Level.FINER; Logger.root.onRecord.listen((event) { final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; @@ -51,6 +51,7 @@ class App extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { + final settingsService = getIt(); return AnimatedBuilder( builder: (context, child) { return Portal( @@ -111,9 +112,15 @@ class App extends StatelessWidget { useInheritedMediaQuery: true, locale: DevicePreview.locale(context), builder: DevicePreview.appBuilder, - theme: AppTheme.light, - darkTheme: AppTheme.dark, - themeMode: getIt().themeMode, + theme: buildTheme( + brightness: Brightness.light, + blindnessType: settingsService.colorBlindnessType, + ), + darkTheme: buildTheme( + brightness: Brightness.dark, + blindnessType: settingsService.colorBlindnessType, + ), + themeMode: settingsService.themeMode, debugShowCheckedModeBanner: false, scrollBehavior: AppScrollingBehavior(), routerDelegate: appRouter.routerDelegate, diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f1eea7e..ee36300 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,3 +1,5 @@ +import 'package:color_blindness/color_blindness.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -20,6 +22,7 @@ class SettingsScreen extends StatelessWidget { children: [ buildLowBandwidthWidget(settings), buildThemeWidget(settings), + if (!kReleaseMode) buildColorBlindnessTestSettings(settings), ], ), ), @@ -55,4 +58,18 @@ class SettingsScreen extends StatelessWidget { ), ); } + + Widget buildColorBlindnessTestSettings(SettingsService settings) { + return ListTile( + title: const Text('Color Blindness Testing'), + trailing: DropdownButton( + value: settings.colorBlindnessType, + items: ColorBlindnessType.values + .map((c) => DropdownMenuItem(value: c, child: Text(c.name))) + .toList(), + onChanged: (value) { + settings.colorBlindnessType = value ?? ColorBlindnessType.none; + }), + ); + } } diff --git a/lib/services/setting_service.dart b/lib/services/setting_service.dart index 58bf0ed..44b0220 100644 --- a/lib/services/setting_service.dart +++ b/lib/services/setting_service.dart @@ -1,3 +1,4 @@ +import 'package:color_blindness/color_blindness.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -29,6 +30,16 @@ class SettingsService extends ChangeNotifier { notifyListeners(); } + ColorBlindnessType _colorBlindnessType = ColorBlindnessType.none; + + ColorBlindnessType get colorBlindnessType => _colorBlindnessType; + + set colorBlindnessType(ColorBlindnessType type) { + _colorBlindnessType = type; + _prefs.setString(_colorBlindnessTestingModeKey, type.name); + notifyListeners(); + } + Future initialize() async { if (_initialized) { return; @@ -36,9 +47,22 @@ class SettingsService extends ChangeNotifier { _prefs = await SharedPreferences.getInstance(); _lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false; _themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey)); + _colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs); _initialized = true; } } const _lowBandwidthModeKey = 'LowBandwidthMode'; const _themeModeKey = 'ThemeMode'; +const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode'; + +ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) { + final cbString = prefs.getString(_colorBlindnessTestingModeKey); + if (cbString?.isEmpty ?? true) { + return ColorBlindnessType.none; + } + return ColorBlindnessType.values.firstWhere( + (c) => c.name == cbString, + orElse: () => ColorBlindnessType.none, + ); +} diff --git a/pubspec.lock b/pubspec.lock index cdfd75b..de4b89f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.3.6" + version: "3.3.7" args: dependency: transitive description: @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + color_blindness: + dependency: "direct main" + description: + name: color_blindness + sha256: "8e85c212aa21ed74e7067ed7ff0a3dce39a366023bd4ca17820981dc8681a6e6" + url: "https://pub.dev" + source: hosted + version: "0.1.2" convert: dependency: transitive description: @@ -261,10 +269,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -309,10 +317,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "0d923fb610d0abf67f2149c3a50ef85f78bebecfc4d645719ca70bcf4abc788f" + sha256: dcde5ad1a0cebcf3715ea3f24d0db1888bf77027a26c77d7779e8ef63b8ade62 url: "https://pub.dev" source: hosted - version: "5.2.7" + version: "5.2.9" fixnum: dependency: transitive description: @@ -452,18 +460,18 @@ packages: dependency: "direct main" description: name: flutter_web_auth_2 - sha256: "6aebfb1797bb1dd38cd32753832670482792e4f0b9cef0329d357f889dbf07c9" + sha256: "354002de1cf644b98af9b1b8c7a0f50f55d738667a54786ae4197e6ff87b224a" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" flutter_web_auth_2_platform_interface: dependency: transitive description: name: flutter_web_auth_2_platform_interface - sha256: dd934033564cacff127b4776798dc2b27b2f2ebfd6b669746455b91c3611cfde + sha256: "91ff7f0bf4ca530aabf857433db2fbcc3d1b8e3c5347ecf58e5ace8f9d29edb0" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" flutter_web_plugins: dependency: transitive description: flutter @@ -537,10 +545,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: feab99a20fd248c658c923ba98f4449ca6e575c3dee9fdf07146f4f33482c6bc + sha256: "99c7fbd4f73da5268046374576af2f6fef970088038d626c5c689a359479af40" url: "https://pub.dev" source: hosted - version: "6.5.5" + version: "6.5.7" graphs: dependency: transitive description: @@ -593,18 +601,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: cb25f04595a88450970dbe727243ba8cd21b6f7e0d7d1fc5b789fc6f52e95494 + sha256: f202f5d730eb8219e35e80c4461fb3a779940ad30ce8fde1586df756e3af25e6 url: "https://pub.dev" source: hosted - version: "0.8.7+1" + version: "0.8.7+3" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: dfb5b0f28b8786fcc662b7ed42bfb4b82a6cbbd74da1958384b10d40bdf212a7 + sha256: "1ea6870350f56af8dab716459bd9d5dc76947e29e07a2ba1d0c172eaaf4f269c" url: "https://pub.dev" source: hosted - version: "0.8.6+6" + version: "0.8.6+7" image_picker_for_web: dependency: transitive description: @@ -617,10 +625,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: d4cb8ab04f770dab9d04c7959e5f6d22e8c5280343d425f9344f93832cf58445 + sha256: a1546ff5861fc15812953d4733b520c3d371cec3d2859a001ff04c46c4d81883 url: "https://pub.dev" source: hosted - version: "0.8.7+2" + version: "0.8.7+3" image_picker_platform_interface: dependency: transitive description: @@ -705,58 +713,58 @@ packages: dependency: "direct main" description: name: media_kit - sha256: defc249a792bf39346e6ee3ec40bbe48685c54aab86a25c4c27cc54a99afb6b1 + sha256: "4c2b3bb600c063ad194934ea7439bce3135cc6b4f9555222c3dc12ca1aa1d85c" url: "https://pub.dev" source: hosted - version: "0.0.4+1" + version: "0.0.5+1" media_kit_libs_ios_video: dependency: "direct main" description: name: media_kit_libs_ios_video - sha256: a6ee06d466f539d161ef3b0c13f101713fd051d90c435a503420cb6a6f4c6e66 + sha256: "28c6ddd5ed43263641293832c8d1fb3f24af81b4eba0b61d6da9bedadbf2e1b1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" media_kit_libs_linux: dependency: "direct main" description: name: media_kit_libs_linux - sha256: "7310b17dd2abc2e7363f78a273086445c2216c7b6dfb60933ca3814031d03814" + sha256: "21acc71cbae3518b3aeef9023a6a3a3decb579a40153764333814987ccd61040" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" media_kit_libs_macos_video: dependency: "direct main" description: name: media_kit_libs_macos_video - sha256: b3259875e201ec66d98ed33b32687ff14fcce7d91e74d420733c039556dff8cd + sha256: ab1cbdf51400e30a9087bd7d6e10c6130d17296e76313fedd0ef0c57dae8c0f4 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" media_kit_libs_windows_video: dependency: "direct main" description: name: media_kit_libs_windows_video - sha256: c2bcbe31e6e6f1e6ae5813c5fe9b7bfde1110c2e95643869919972a7845948e1 + sha256: "99a3a85b185ae012a8e3bd596cf0ca425834477d0bec86207548d6ff7c926254" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" media_kit_native_event_loop: dependency: "direct main" description: name: media_kit_native_event_loop - sha256: "677ea41d13a2013ca7fe050674eac3b5d891185d4300779727a5860a85cdda60" + sha256: ed87140ad4b64156b2b470c8105f48d8cad7923c952ca05d23e02d28978d2cb3 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" media_kit_video: dependency: "direct main" description: name: media_kit_video - sha256: "1a870a3d731a9ce34e12ec5baed05b3081164f0c85b71baf05900ca47ee5639c" + sha256: "3860b1e8b2779a5702ecea7e6e3d8b16a79407ad478234008550cca660ac52ad" url: "https://pub.dev" source: hosted - version: "0.0.4" + version: "0.0.6" meta: dependency: transitive description: @@ -865,18 +873,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.25" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_linux: dependency: transitive description: @@ -937,10 +945,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: c3120a968135aead39699267f4c74bc9a08e4e909e86bc1b0af5bfd78691123c + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.7.2" + version: "3.7.3" pool: dependency: transitive description: @@ -1025,18 +1033,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6" + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_linux: dependency: transitive description: @@ -1126,10 +1134,10 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: "822d321a008e194d7929357e5b58d2e4a04ab670d137182f9759152aa33180ff" + sha256: a3ba4b66a7ab170ce7aa3f5ac43c19ee8d6637afbe7b7c95c94112b4f4d91566 url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.11.0" stack_trace: dependency: transitive description: @@ -1238,18 +1246,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411 url: "https://pub.dev" source: hosted - version: "6.0.26" + version: "6.0.27" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -1262,10 +1270,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: @@ -1326,10 +1334,10 @@ packages: dependency: transitive description: name: video_player_avfoundation - sha256: af308d08c672d5ff718c60127665249617c37a709cb8f0a18dd28a0360299b7c + sha256: "75c6d68cd479a25f34d635149ba6887bc8f1b2b2921841121fd44ea0c5bc1927" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.4" video_player_platform_interface: dependency: transitive description: @@ -1358,18 +1366,18 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" window_to_front: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 03b4995..cbc3cee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: device_info_plus: ^8.0.0 string_validator: ^0.3.0 device_preview: ^1.1.0 + color_blindness: ^0.1.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 75c7ec2..6b3d3e0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopWindowPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); ObjectboxFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f149558..f4d8ce9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_window flutter_secure_storage_windows + media_kit_libs_windows_video media_kit_video objectbox_flutter_libs url_launcher_windows @@ -12,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - media_kit_libs_windows_video media_kit_native_event_loop ) From 86076f23b96cd2bb059abf327fc7266035a2d4fe Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 09:59:44 -0400 Subject: [PATCH 09/20] Disable display_preview for now... --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index b6b2e32..a28781e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,7 @@ import 'utils/old_android_letsencrypte_cert.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // await dotenv.load(fileName: '.env'); - const enablePreview = true; + const enablePreview = false; Logger.root.level = Level.FINER; Logger.root.onRecord.listen((event) { final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; From 5f7a45954f1545226cce1c476c7daf8e452cd547 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 11:45:45 -0400 Subject: [PATCH 10/20] Fix low-bandwidth media stand in cards being wrong height --- lib/controls/timeline/flattened_tree_entry_control.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/controls/timeline/flattened_tree_entry_control.dart b/lib/controls/timeline/flattened_tree_entry_control.dart index dde4d4c..f2d46fd 100644 --- a/lib/controls/timeline/flattened_tree_entry_control.dart +++ b/lib/controls/timeline/flattened_tree_entry_control.dart @@ -167,6 +167,7 @@ class _StatusControlState extends State { width: items.length > 1 ? ResponsiveSizesCalculator(context).maxThumbnailWidth : ResponsiveSizesCalculator(context).viewPortalWidth, + height: ResponsiveSizesCalculator(context).maxThumbnailHeight, ); }, separatorBuilder: (context, index) { From da6183d7bb56c0f61fb68832b3e5068545618979 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 11:46:14 -0400 Subject: [PATCH 11/20] Fix media_kit player startup quirks with new version --- .../audio_video/media_kit_av_control.dart | 19 ++++++++++++++++--- lib/main.dart | 2 ++ pubspec.lock | 16 ++++++++-------- pubspec.yaml | 16 ++++++++-------- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/controls/audio_video/media_kit_av_control.dart b/lib/controls/audio_video/media_kit_av_control.dart index 305ccf3..ecaa3ab 100644 --- a/lib/controls/audio_video/media_kit_av_control.dart +++ b/lib/controls/audio_video/media_kit_av_control.dart @@ -21,7 +21,11 @@ class MediaKitAvControl extends StatefulWidget { class _MediaKitAvControlState extends State { static final _logger = Logger('$MediaKitAvControl'); - final player = Player(); + final Player player = Player( + configuration: const PlayerConfiguration( + logLevel: MPVLogLevel.warn, + ), + ); VideoController? controller; var needToOpen = true; @@ -78,8 +82,17 @@ class _MediaKitAvControlState extends State { Widget build(BuildContext context) { print('Building MediaKit Control'); if (controller == null) { - return const Center( - child: CircularProgressIndicator(), + return Container( + width: widget.width, + height: widget.height, + color: Colors.black12, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + CircularProgressIndicator(), + ], + ), ); } diff --git a/lib/main.dart b/lib/main.dart index a28781e..ef3f5a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:device_preview/device_preview.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; import 'package:provider/provider.dart'; @@ -26,6 +27,7 @@ import 'utils/old_android_letsencrypte_cert.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + MediaKit.ensureInitialized(); // await dotenv.load(fileName: '.env'); const enablePreview = false; Logger.root.level = Level.FINER; diff --git a/pubspec.lock b/pubspec.lock index de4b89f..6d1252a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -717,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5+1" + media_kit_libs_android_video: + dependency: "direct main" + description: + name: media_kit_libs_android_video + sha256: "76fa95350b472f9dc163dd11e0145133c2e05456c06d0febb4e189922a68bd3c" + url: "https://pub.dev" + source: hosted + version: "1.0.0" media_kit_libs_ios_video: dependency: "direct main" description: @@ -773,14 +781,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" - metadata_fetch: - dependency: "direct main" - description: - name: metadata_fetch - sha256: "2c79e69e71cbb051041da6a8a9dd3df0617db84482ab3f36b4952ff779b7580e" - url: "https://pub.dev" - source: hosted - version: "0.4.1" mime: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cbc3cee..163d33d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,14 +26,14 @@ dependencies: image_picker: ^0.8.6 logging: ^1.1.0 markdown: ^7.0.1 - media_kit: ^0.0.3 - media_kit_libs_ios_video: ^1.0.2 - media_kit_libs_linux: ^1.0.1 - media_kit_libs_macos_video: ^1.0.2 - media_kit_libs_windows_video: ^1.0.1 - media_kit_native_event_loop: ^1.0.2 - media_kit_video: ^0.0.4 - metadata_fetch: ^0.4.1 + media_kit: ^0.0.5 # Primary package. + media_kit_video: ^0.0.6 # For video rendering. + media_kit_native_event_loop: ^1.0.3 # Support for higher number of concurrent instances & better performance. + media_kit_libs_windows_video: ^1.0.2 # Windows package for video native libraries. + media_kit_libs_android_video: ^1.0.0 # Android package for video native libraries. + media_kit_libs_macos_video: ^1.0.4 # macOS package for video native libraries. + media_kit_libs_ios_video: ^1.0.4 # iOS package for video native libraries. + media_kit_libs_linux: ^1.0.2 # GNU/Linux dependency package. metadata_fetch: ^0.4.1 multi_trigger_autocomplete: ^0.1.1 network_to_file_image: ^4.0.1 path: ^1.8.2 From 40f23d04ed1da284f52c75f667f3872a994595e4 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 18:24:16 -0400 Subject: [PATCH 12/20] Bump Result Monad library to 2.2.0 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 6d1252a..3ab0072 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -993,10 +993,10 @@ packages: dependency: "direct main" description: name: result_monad - sha256: "59e65e969f93c8aff18104f36233b0fd102a096d6501d3515e2a80cd67f3565a" + sha256: "125e17cd16e2fd3716b67a9833eaaaaa5c4090a18e4eb96783fc165e57a57a9b" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 163d33d..68ac4f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: network_to_file_image: ^4.0.1 path: ^1.8.2 provider: ^6.0.4 - result_monad: ^2.1.0 + result_monad: ^2.2.0 scrollable_positioned_list: ^0.3.5 shared_preferences: ^2.0.15 sqlite3: ^1.9.1 From e8c3b6616a24bd4c21032c0fe645f6b6bb30c228 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 19 Apr 2023 18:24:28 -0400 Subject: [PATCH 13/20] Make group members result come back alphabetical --- lib/services/connections_manager.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index b2c3409..59e9925 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -208,7 +208,14 @@ class ConnectionsManager extends ChangeNotifier { } Result, ExecError> getGroupMembers(GroupData group) { - return groupsRepo.getGroupMembers(group); + return groupsRepo + .getGroupMembers(group) + .transform( + (members) => members + ..sort((c1, c2) => + c1.name.toLowerCase().compareTo(c2.name.toLowerCase())), + ) + .execErrorCast(); } FutureResult createGroup(String newName) async { From 3e70dcbd742a288bda60b206cb498084f034a2ee Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Thu, 20 Apr 2023 12:14:04 -0400 Subject: [PATCH 14/20] Add Group Editor ability to remove users from group --- lib/screens/group_editor_screen.dart | 78 ++++++++++++++++++++++++++- lib/services/connections_manager.dart | 14 ++--- pubspec.lock | 11 ++-- pubspec.yaml | 4 +- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart index 69a4adf..f51b376 100644 --- a/lib/screens/group_editor_screen.dart +++ b/lib/screens/group_editor_screen.dart @@ -6,7 +6,9 @@ import 'package:result_monad/result_monad.dart'; import '../controls/linear_status_indicator.dart'; import '../controls/padding.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 '../services/connections_manager.dart'; import '../services/network_status_service.dart'; @@ -26,6 +28,7 @@ class _GroupEditorScreenState extends State { final groupTextController = TextEditingController(); var processingUpdate = false; var allowNameEditing = false; + var filterText = ''; late GroupData groupData; Future updateGroupName( @@ -66,6 +69,24 @@ class _GroupEditorScreenState extends State { } } + 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', + ); + if (context.mounted) { + buildSnackbar(context, message); + } + } + } + @override void initState() { super.initState(); @@ -89,7 +110,16 @@ class _GroupEditorScreenState extends State { .activeEntry .value; - final members = manager.getGroupMembers(groupData).getValueOrElse(() => []); + 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( @@ -161,13 +191,57 @@ class _GroupEditorScreenState extends State { const VerticalPadding(), ElevatedButton( onPressed: () {}, child: const Text('Add Users')), + 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( - title: Text(m.name), + title: Text( + '${m.name} (${m.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${m.lastStatus?.toIso8601String() ?? "Unknown"}'), + trailing: IconButton( + onPressed: () async => + removeUserFromGroup(manager, m), + icon: const Icon(Icons.remove), + ), ); }, separatorBuilder: (_, __) => const Divider(), diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 59e9925..6fc1115 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -312,17 +312,17 @@ class ConnectionsManager extends ChangeNotifier { FutureResult removeUserFromGroup( GroupData group, Connection connection) async { _logger.finest('Removing ${connection.name} from group: ${group.name}'); - final result = await GroupsClient(getIt().currentProfile) - .removeConnectionFromGroup(group, connection); - result.match( - onSuccess: (_) => _refreshGroupListData(connection.id, true), - onError: (error) { + return GroupsClient(getIt().currentProfile) + .removeConnectionFromGroup(group, connection) + .withResultAsync((_) async => refreshGroupMemberships(group)) + .withResultAsync((_) async => notifyListeners()) + .mapError( + (error) { _logger.severe( 'Error removing ${connection.name} from group: ${group.name}'); + return error; }, ); - - return result.execErrorCast(); } Result getById(String id) { diff --git a/pubspec.lock b/pubspec.lock index 3ab0072..52712b8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -992,11 +992,12 @@ packages: result_monad: dependency: "direct main" description: - name: result_monad - sha256: "125e17cd16e2fd3716b67a9833eaaaaa5c4090a18e4eb96783fc165e57a57a9b" - url: "https://pub.dev" - source: hosted - version: "2.2.0" + path: "." + ref: HEAD + resolved-ref: "12a2ae1e0830f4aff32f1d94835901de545cf917" + url: "https://gitlab.com/HankG/dart-result-monad.git" + source: git + version: "2.3.0" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 68ac4f9..fca9d5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,9 @@ dependencies: network_to_file_image: ^4.0.1 path: ^1.8.2 provider: ^6.0.4 - result_monad: ^2.2.0 + result_monad: + git: + url: https://gitlab.com/HankG/dart-result-monad.git scrollable_positioned_list: ^0.3.5 shared_preferences: ^2.0.15 sqlite3: ^1.9.1 From 32408b96c024e303a268c93b49351c2c6741bdac Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Thu, 20 Apr 2023 16:26:11 -0400 Subject: [PATCH 15/20] Add stack trace logging and context.mounted check in buildSnackBar --- .../media_uploads_control.dart | 10 +++--- lib/controls/notifications_control.dart | 12 ++++--- .../timeline/interactions_bar_control.dart | 3 ++ lib/models/exec_error.dart | 15 +++++++- lib/screens/editor.dart | 34 ++++++++++++------- lib/screens/group_create_screen.dart | 8 ++--- lib/screens/group_editor_screen.dart | 8 ++--- lib/utils/snackbar_builder.dart | 3 ++ pubspec.lock | 2 +- pubspec.yaml | 1 + 10 files changed, 61 insertions(+), 35 deletions(-) diff --git a/lib/controls/entry_media_attachments/media_uploads_control.dart b/lib/controls/entry_media_attachments/media_uploads_control.dart index b2af863..ed3bcc7 100644 --- a/lib/controls/entry_media_attachments/media_uploads_control.dart +++ b/lib/controls/entry_media_attachments/media_uploads_control.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:relatica/models/exec_error.dart'; import 'package:result_monad/result_monad.dart'; import '../../models/gallery_data.dart'; @@ -55,10 +56,9 @@ class _MediaUploadsControlState extends State { .entryMediaItems.attachments .addAll(newEntries)), onError: (error) { - if (mounted) { - buildSnackbar(context, - 'Error selecting attachments: $error'); - } + buildSnackbar(context, + 'Error selecting attachments: $error'); + logError(error, _logger); }); }, icon: const Icon(Icons.camera_alt), @@ -111,7 +111,7 @@ class _MediaUploadsControlState extends State { alignLabelWithHint: true, border: OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, ), borderRadius: BorderRadius.circular(5.0), ), diff --git a/lib/controls/notifications_control.dart b/lib/controls/notifications_control.dart index f0a1d49..459698d 100644 --- a/lib/controls/notifications_control.dart +++ b/lib/controls/notifications_control.dart @@ -3,8 +3,10 @@ import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import 'package:result_monad/result_monad.dart'; import '../globals.dart'; +import '../models/exec_error.dart'; import '../models/user_notification.dart'; import '../routes.dart'; import '../services/connections_manager.dart'; @@ -139,11 +141,11 @@ class NotificationControl extends StatelessWidget { onPressed: manager == null ? null : () async { - final result = await manager.markSeen(notification); - if (result.isFailure) { - buildSnackbar(context, - 'Error marking notification: ${result.error}'); - } + await manager.markSeen(notification).withError((error) { + buildSnackbar( + context, 'Error marking notification: $error'); + logError(error, _logger); + }); }, icon: Icon(Icons.close_rounded)), ); diff --git a/lib/controls/timeline/interactions_bar_control.dart b/lib/controls/timeline/interactions_bar_control.dart index cca4239..831879d 100644 --- a/lib/controls/timeline/interactions_bar_control.dart +++ b/lib/controls/timeline/interactions_bar_control.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; +import 'package:relatica/models/exec_error.dart'; import 'package:result_monad/result_monad.dart'; import '../../globals.dart'; @@ -102,6 +103,7 @@ class _InteractionsBarControlState extends State { }); }, onError: (error) { buildSnackbar(context, 'Error resharing post by ${widget.entry.author}'); + logError(error, _logger); }); setState(() { isProcessing = false; @@ -128,6 +130,7 @@ class _InteractionsBarControlState extends State { }, onError: (error) { buildSnackbar( context, 'Error un-resharing post by ${widget.entry.author}'); + logError(error, _logger); }); setState(() { isProcessing = false; diff --git a/lib/models/exec_error.dart b/lib/models/exec_error.dart index 5653fc5..d13b723 100644 --- a/lib/models/exec_error.dart +++ b/lib/models/exec_error.dart @@ -1,4 +1,6 @@ +import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; +import 'package:stack_trace/stack_trace.dart'; Result buildErrorResult({ required ErrorType type, @@ -14,22 +16,28 @@ Result buildErrorResult({ class ExecError { final ErrorType type; final String message; + final Trace trace; - ExecError({required this.type, this.message = ''}); + ExecError({required this.type, this.message = '', Trace? trace}) + : trace = trace ?? Trace.current(1); ExecError copy({ ErrorType? type, String? message, + Trace? trace, }) => ExecError( type: type ?? this.type, message: message ?? this.message, + trace: trace ?? this.trace, ); @override String toString() { return 'ExecError{type: $type, message: $message}'; } + + String printStackTrace() => trace.terse.toString(); } enum ErrorType { @@ -50,3 +58,8 @@ extension ExecErrorExtension on Result { FutureResult execErrorCastAsync() async => execErrorCast(); } + +void logError(ExecError error, Logger logger, + {Level logLevel = Level.INFO, String message = ''}) { + logger.log(logLevel, '$message $error\n${error.trace}'); +} diff --git a/lib/screens/editor.dart b/lib/screens/editor.dart index dc82777..a28c2af 100644 --- a/lib/screens/editor.dart +++ b/lib/screens/editor.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; import 'package:provider/provider.dart'; +import 'package:result_monad/result_monad.dart'; import 'package:uuid/uuid.dart'; import '../controls/autocomplete/hashtag_autocomplete_options.dart'; @@ -15,6 +16,7 @@ import '../controls/padding.dart'; import '../controls/standard_appbar.dart'; import '../controls/timeline/status_header_control.dart'; import '../globals.dart'; +import '../models/exec_error.dart'; import '../models/group_data.dart'; import '../models/image_entry.dart'; import '../models/link_preview_data.dart'; @@ -110,13 +112,8 @@ class _EditorScreenState extends State { loaded = true; }); }, onError: (error) { - if (context.mounted) { - buildSnackbar( - context, - 'Error getting post for editing: $error', - ); - } - _logger.severe('Error getting post for editing: $error'); + buildSnackbar(context, 'Error getting post for editing: $error'); + logError(error, _logger); }); } @@ -152,20 +149,25 @@ class _EditorScreenState extends State { isSubmitting = true; }); - final result = await manager.createNewStatus( + final result = await manager + .createNewStatus( bodyText, spoilerText: spoilerController.text, inReplyToId: widget.parentId, newMediaItems: newMediaItems, existingMediaItems: existingMediaItems, visibility: visibility, - ); + ) + .withError((error) { + buildSnackbar(context, 'Error posting: $error'); + logError(error, _logger); + }); + setState(() { isSubmitting = false; }); if (result.isFailure) { - buildSnackbar(context, 'Error posting: ${result.error}'); return; } @@ -184,7 +186,8 @@ class _EditorScreenState extends State { isSubmitting = true; }); - final result = await manager.editStatus( + final result = await manager + .editStatus( widget.id, bodyText, spoilerText: spoilerController.text, @@ -192,13 +195,17 @@ class _EditorScreenState extends State { newMediaItems: newMediaItems, existingMediaItems: existingMediaItems, newMediaItemVisibility: visibility, - ); + ) + .withError((error) { + buildSnackbar(context, 'Error updating $statusType: $error'); + logError(error, _logger); + }); + setState(() { isSubmitting = false; }); if (result.isFailure) { - buildSnackbar(context, 'Error Updating $statusType: ${result.error}'); return; } @@ -468,6 +475,7 @@ class _EditorScreenState extends State { if (mounted) { buildSnackbar( context, 'Error building link preview: $error'); + logError(error, _logger); } }); }, diff --git a/lib/screens/group_create_screen.dart b/lib/screens/group_create_screen.dart index c5c522c..6797111 100644 --- a/lib/screens/group_create_screen.dart +++ b/lib/screens/group_create_screen.dart @@ -27,10 +27,10 @@ class _GroupCreateScreenState extends State { final result = await manager.createGroup(groupTextController.text); if (context.mounted) { result.match( - onSuccess: (_) => context.canPop() ? context.pop() : null, - onError: (error) => - buildSnackbar(context, 'Error trying to create new group: $error'), - ); + onSuccess: (_) => context.canPop() ? context.pop() : null, + onError: (error) { + buildSnackbar(context, 'Error trying to create new group: $error'); + }); } } diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart index f51b376..23e4a4c 100644 --- a/lib/screens/group_editor_screen.dart +++ b/lib/screens/group_editor_screen.dart @@ -46,9 +46,7 @@ class _GroupEditorScreenState extends State { allowNameEditing = false; }); }, onError: (error) { - if (mounted) { - buildSnackbar(context, 'Error renaming group: $error'); - } + buildSnackbar(context, 'Error renaming group: $error'); }); } else { groupTextController.text = groupData.name; @@ -81,9 +79,7 @@ class _GroupEditorScreenState extends State { onSuccess: (_) => 'Removed $messageBase', onError: (error) => 'Error removing $messageBase: $error', ); - if (context.mounted) { - buildSnackbar(context, message); - } + buildSnackbar(context, message); } } diff --git a/lib/utils/snackbar_builder.dart b/lib/utils/snackbar_builder.dart index f34ec83..9751204 100644 --- a/lib/utils/snackbar_builder.dart +++ b/lib/utils/snackbar_builder.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; Future buildSnackbar(BuildContext context, String message, {int durationSec = 3}) async { + if (!context.mounted) { + return; + } final snackBar = SnackBar( content: SelectableText(message), duration: Duration(seconds: durationSec), diff --git a/pubspec.lock b/pubspec.lock index 52712b8..bc159db 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1140,7 +1140,7 @@ packages: source: hosted version: "1.11.0" stack_trace: - dependency: transitive + dependency: "direct main" description: name: stack_trace sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 diff --git a/pubspec.yaml b/pubspec.yaml index fca9d5c..e9bbde4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: string_validator: ^0.3.0 device_preview: ^1.1.0 color_blindness: ^0.1.2 + stack_trace: ^1.11.0 dev_dependencies: flutter_test: From 870a7fc2a0ecc8300acdc7fafb025c29dfc6e6a7 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Thu, 20 Apr 2023 16:26:18 -0400 Subject: [PATCH 16/20] Add stack trace logging and context.mounted check in buildSnackBar --- lib/screens/messages_new_thread.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/messages_new_thread.dart b/lib/screens/messages_new_thread.dart index 35f5477..c54302b 100644 --- a/lib/screens/messages_new_thread.dart +++ b/lib/screens/messages_new_thread.dart @@ -27,7 +27,7 @@ class MessagesNewThread extends StatelessWidget { Widget buildBody(BuildContext context) { final border = OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, ), borderRadius: BorderRadius.circular(5.0), ); From cd7ca5776b712ea2c06af53386d7ed1016de59a0 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 21 Apr 2023 09:52:58 -0400 Subject: [PATCH 17/20] Add additional handle and last post time to contacts screen list --- lib/screens/contacts_screen.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 69293a3..17d9163 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -64,7 +64,14 @@ class _ContactsScreenState extends State { context.pushNamed(ScreenPaths.userProfile, params: {'id': contact.id}); }, - title: Text(contact.name), + title: Text( + '${contact.name} (${contact.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', + softWrap: true, + ), trailing: Text(contact.status.label()), ); }, From 646f576016cc3aa858c7cde05e5a13ec8751277a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 21 Apr 2023 09:54:31 -0400 Subject: [PATCH 18/20] Add GroupAddUsersScreen and route --- lib/routes.dart | 6 + lib/screens/group_add_users_screen.dart | 188 ++++++++++++++++++++++++ lib/services/connections_manager.dart | 22 ++- 3 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 lib/screens/group_add_users_screen.dart diff --git a/lib/routes.dart b/lib/routes.dart index f982e11..4041deb 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -7,6 +7,7 @@ import 'screens/editor.dart'; import 'screens/follow_request_adjudication_screen.dart'; import 'screens/gallery_browsers_screen.dart'; import 'screens/gallery_screen.dart'; +import 'screens/group_add_users_screen.dart'; import 'screens/group_create_screen.dart'; import 'screens/group_editor_screen.dart'; import 'screens/group_management_screen.dart'; @@ -139,6 +140,11 @@ final appRouter = GoRouter( path: 'new', builder: (context, state) => GroupCreateScreen(), ), + GoRoute( + path: 'add_users/:id', + builder: (context, state) => + GroupAddUsersScreen(groupId: state.params['id']!), + ), ], ), GoRoute( diff --git a/lib/screens/group_add_users_screen.dart b/lib/screens/group_add_users_screen.dart new file mode 100644 index 0000000..6a73143 --- /dev/null +++ b/lib/screens/group_add_users_screen.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../controls/linear_status_indicator.dart'; +import '../controls/responsive_max_width.dart'; +import '../controls/status_and_refresh_button.dart'; +import '../globals.dart'; +import '../models/connection.dart'; +import '../models/exec_error.dart'; +import '../models/group_data.dart'; +import '../routes.dart'; +import '../services/auth_service.dart'; +import '../services/connections_manager.dart'; +import '../services/network_status_service.dart'; +import '../utils/active_profile_selector.dart'; +import '../utils/snackbar_builder.dart'; + +class GroupAddUsersScreen extends StatefulWidget { + final String groupId; + + const GroupAddUsersScreen({super.key, required this.groupId}); + + @override + State createState() => _GroupAddUsersScreenState(); +} + +class _GroupAddUsersScreenState extends State { + static final _logger = Logger('$GroupAddUsersScreen'); + var filterText = ''; + late GroupData groupData; + + @override + void initState() { + super.initState(); + final manager = + getIt>().activeEntry.value; + groupData = + manager.getMyGroups().where((g) => g.id == widget.groupId).first; + } + + Future addUserToGroup( + ConnectionsManager manager, + Connection connection, + ) async { + final messageBase = '${connection.name} from ${groupData.name}'; + final confirm = await showYesNoDialog(context, 'Add $messageBase?'); + if (context.mounted && confirm == true) { + final message = await manager + .addUserToGroup(groupData, connection) + .withResult((p0) => setState(() {})) + .fold( + onSuccess: (_) => 'Added $messageBase', + onError: (error) => 'Error adding $messageBase: $error', + ); + buildSnackbar(context, message); + } + } + + @override + Widget build(BuildContext context) { + _logger.finer('Build'); + final nss = getIt(); + final activeProfile = context.watch(); + final manager = context + .watch>() + .activeEntry + .value; + final groupMembers = manager + .getGroupMembers(groupData) + .withError((e) => logError(e, _logger)) + .getValueOrElse(() => []) + .toSet(); + final allContacts = manager.getMyContacts(); + final filterTextLC = filterText.toLowerCase(); + final contacts = allContacts + .where((c) => !groupMembers.contains(c)) + .where((c) => + filterText.isEmpty || + c.name.toLowerCase().contains(filterTextLC) || + c.handle.toLowerCase().contains(filterTextLC)) + .toList(); + contacts.sort((c1, c2) => c1.name.compareTo(c2.name)); + _logger.finer( + () => + '# in group: ${groupMembers.length} # Contacts: ${allContacts.length}, #filtered: ${contacts.length}', + ); + late Widget body; + if (contacts.isEmpty) { + body = const SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Center( + child: Text('No contacts'), + )); + } else { + body = ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile( + onTap: () { + context.pushNamed(ScreenPaths.userProfile, + params: {'id': contact.id}); + }, + title: Text( + '${contact.name} (${contact.handle})', + softWrap: true, + ), + subtitle: Text( + 'Last Status: ${contact.lastStatus?.toIso8601String() ?? "Unknown"}', + softWrap: true, + ), + trailing: IconButton( + onPressed: () async => await addUserToGroup(manager, contact), + icon: const Icon(Icons.add)), + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: contacts.length); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Add Users'), + ), + body: SafeArea( + child: RefreshIndicator( + onRefresh: () async { + if (nss.connectionUpdateStatus.value) { + return; + } + manager.refreshGroupMemberships(groupData); + return; + }, + child: ResponsiveMaxWidth( + child: Column( + children: [ + Text( + 'Group: ${groupData.name}', + style: Theme.of(context).textTheme.bodyLarge, + softWrap: true, + ), + 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), + ), + ) + ], + ), + StandardLinearProgressIndicator(nss.connectionUpdateStatus), + Expanded(child: body), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 6fc1115..63940ee 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -296,17 +296,15 @@ class ConnectionsManager extends ChangeNotifier { FutureResult addUserToGroup( GroupData group, Connection connection) async { _logger.finest('Adding ${connection.name} to group: ${group.name}'); - final result = await GroupsClient(getIt().currentProfile) - .addConnectionToGroup(group, connection); - result.match( - onSuccess: (_) => _refreshGroupListData(connection.id, true), - onError: (error) { - _logger - .severe('Error adding ${connection.name} to group: ${group.name}'); - }, - ); - - return result.execErrorCast(); + return await GroupsClient(getIt().currentProfile) + .addConnectionToGroup(group, connection) + .withResultAsync((_) async => refreshGroupMemberships(group)) + .withResult((_) => notifyListeners()) + .mapError((error) { + _logger + .severe('Error adding ${connection.name} from group: ${group.name}'); + return error; + }); } FutureResult removeUserFromGroup( @@ -315,7 +313,7 @@ class ConnectionsManager extends ChangeNotifier { return GroupsClient(getIt().currentProfile) .removeConnectionFromGroup(group, connection) .withResultAsync((_) async => refreshGroupMemberships(group)) - .withResultAsync((_) async => notifyListeners()) + .withResult((_) => notifyListeners()) .mapError( (error) { _logger.severe( From abe521072896a85a8af15f33f7853b295f5e77df Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 21 Apr 2023 09:54:34 -0400 Subject: [PATCH 19/20] Tweak titling and add user button, wrap in ResponsiveMaxWidth --- lib/screens/group_editor_screen.dart | 213 +++++++++++++++------------ 1 file changed, 115 insertions(+), 98 deletions(-) diff --git a/lib/screens/group_editor_screen.dart b/lib/screens/group_editor_screen.dart index 23e4a4c..ba9bddd 100644 --- a/lib/screens/group_editor_screen.dart +++ b/lib/screens/group_editor_screen.dart @@ -5,11 +5,13 @@ 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'; @@ -18,7 +20,7 @@ import '../utils/snackbar_builder.dart'; class GroupEditorScreen extends StatefulWidget { final String groupId; - GroupEditorScreen({super.key, required this.groupId}); + const GroupEditorScreen({super.key, required this.groupId}); @override State createState() => _GroupEditorScreenState(); @@ -135,116 +137,131 @@ class _GroupEditorScreenState extends State { onRefresh: () async { manager.refreshGroups(); }, - 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), + 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(), - ElevatedButton( - onPressed: () {}, child: const Text('Add Users')), - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - onChanged: (value) { + const HorizontalPadding(), + IconButton( + onPressed: () { + if (allowNameEditing) { + groupTextController.text = groupData.name; + } setState(() { - filterText = value.toLowerCase(); + allowNameEditing = !allowNameEditing; }); }, - decoration: InputDecoration( - labelText: 'Filter By Name', - alignLabelWithHint: true, - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).highlightColor, + 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), ), - 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( - title: Text( - '${m.name} (${m.handle})', - softWrap: true, + Padding( + padding: const EdgeInsets.all(8.0), + child: StatusAndRefreshButton( + valueListenable: nss.connectionUpdateStatus, + refreshFunction: () async => + manager.refreshGroupMemberships(groupData), ), - subtitle: Text( - 'Last Status: ${m.lastStatus?.toIso8601String() ?? "Unknown"}'), - trailing: IconButton( - onPressed: () async => - removeUserFromGroup(manager, m), - icon: const Icon(Icons.remove), - ), - ); - }, - separatorBuilder: (_, __) => const Divider(), - itemCount: members.length, + ) + ], ), - ), - ], + Expanded( + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, index) { + final m = members[index]; + return ListTile( + 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, + ), + ), + ], + ), ), ), )); From e01d63b69453844a911a430721adb63026e8bbc7 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 21 Apr 2023 10:23:38 -0400 Subject: [PATCH 20/20] Update changelog for new groups stuff and the fixes for contacts management --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f785d..bbf4713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,19 @@ * The "copy text" action now copies the plain text not HTML version. So things like hashtags show up as "#hashtag" not the HTML code around it with the link reference etc. * Clicking on the "Home" button if you already are on that screen scrolls to the top. - * Auto-fill suggestions render at the top of the edit text field not bottom. + * Auto-complete suggestions for hashtags and accounts render at the top of the edit text field not bottom. + * Contacts screen shows user's handle and last post time (as far as your server knows about) for the user * Fixes + * Seemingly disappearing contacts on Contacts screen have been corrected. * New Features * Responsive design for allowing the image and video attachments to scale up for larger videos - but limit content width on very large screens. + but limit timeline/list width on very large screens. + * Groups (Mastodon Lists) management including: + * Creating new groups + * Renaming groups + * Deleting groups + * Add users to groups + * Remove users from groups ## Version 0.4.0 (beta), 04 April 2023