mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +00:00
Merge branch 'group_management' into 'main'
Group management See merge request mysocialportal/relatica!33
This commit is contained in:
commit
09410ab1f3
36 changed files with 1208 additions and 269 deletions
12
CHANGELOG.md
12
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
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,11 @@ class MediaKitAvControl extends StatefulWidget {
|
|||
|
||||
class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
||||
static final _logger = Logger('$MediaKitAvControl');
|
||||
final player = Player();
|
||||
final Player player = Player(
|
||||
configuration: const PlayerConfiguration(
|
||||
logLevel: MPVLogLevel.warn,
|
||||
),
|
||||
);
|
||||
VideoController? controller;
|
||||
var needToOpen = true;
|
||||
|
||||
|
@ -30,7 +34,7 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
|||
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(() {});
|
||||
|
@ -78,8 +82,17 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
|||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MediaUploadsControl> {
|
|||
.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<MediaUploadsControl> {
|
|||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -167,6 +167,7 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|||
width: items.length > 1
|
||||
? ResponsiveSizesCalculator(context).maxThumbnailWidth
|
||||
: ResponsiveSizesCalculator(context).viewPortalWidth,
|
||||
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
|
|
|
@ -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<InteractionsBarControl> {
|
|||
});
|
||||
}, 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<InteractionsBarControl> {
|
|||
}, onError: (error) {
|
||||
buildSnackbar(
|
||||
context, 'Error un-resharing post by ${widget.entry.author}');
|
||||
logError(error, _logger);
|
||||
});
|
||||
setState(() {
|
||||
isProcessing = false;
|
||||
|
|
|
@ -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<Connection> newConnections) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Result<Connection, ExecError> getById(String id);
|
||||
|
||||
bool updateConnection(Connection connection) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Result<Connection, ExecError> getByName(String name);
|
||||
|
||||
Result<Connection, ExecError> getById(String id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
Result<Connection, ExecError> getByHandle(String handle);
|
||||
|
||||
Result<Connection, ExecError> getByName(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
List<Connection> getMyContacts();
|
||||
|
||||
Result<Connection, ExecError> getByHandle(String handle) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
List<Connection> getMyContacts() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
List<Connection> getKnownUsersByName(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
List<Connection> getKnownUsersByName(String name);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../../models/connection.dart';
|
||||
import '../../models/exec_error.dart';
|
||||
import '../../models/group_data.dart';
|
||||
|
||||
class IGroupsRepo {
|
||||
void addAllGroups(List<GroupData> groups) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
abstract class IGroupsRepo {
|
||||
void addAllGroups(List<GroupData> groups);
|
||||
|
||||
void clearMyGroups() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
void addConnectionToGroup(GroupData group, Connection connection);
|
||||
|
||||
List<GroupData> getMyGroups() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
void clearMyGroups();
|
||||
|
||||
Result<List<GroupData>, ExecError> getGroupsForUser(String id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
void upsertGroup(GroupData group);
|
||||
|
||||
bool updateConnectionGroupData(String id, List<GroupData> currentGroups) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
void deleteGroup(GroupData group);
|
||||
|
||||
List<GroupData> getMyGroups();
|
||||
|
||||
Result<List<Connection>, ExecError> getGroupMembers(GroupData group);
|
||||
|
||||
Result<List<GroupData>, ExecError> getGroupsForUser(String id);
|
||||
|
||||
bool updateConnectionGroupData(String id, List<GroupData> currentGroups);
|
||||
}
|
||||
|
|
|
@ -16,25 +16,6 @@ class MemoryConnectionsRepo implements IConnectionsRepo {
|
|||
_myContacts.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
bool addAllConnections(Iterable<Connection> 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<Connection> 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);
|
||||
|
|
|
@ -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 = <String, List<GroupData>>{};
|
||||
final _connectionsForGroup = <String, Set<Connection>>{};
|
||||
final _myGroups = <GroupData>{};
|
||||
|
||||
@override
|
||||
|
@ -25,11 +27,28 @@ class MemoryGroupsRepo implements IGroupsRepo {
|
|||
return _myGroups.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Result<List<Connection>, 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<GroupData> groups) {
|
||||
_myGroups.addAll(groups);
|
||||
|
@ -40,4 +59,20 @@ class MemoryGroupsRepo implements IGroupsRepo {
|
|||
_groupsForConnection[id] = currentGroups;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void upsertGroup(GroupData group) {
|
||||
_connectionsForGroup.putIfAbsent(group.id, () => {});
|
||||
_myGroups.remove(group);
|
||||
_myGroups.add(group);
|
||||
}
|
||||
|
||||
@override
|
||||
void deleteGroup(GroupData group) {
|
||||
for (final conGroups in _groupsForConnection.values) {
|
||||
conGroups.remove(group);
|
||||
}
|
||||
_connectionsForGroup.remove(group.id);
|
||||
_myGroups.remove(group);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,23 +21,6 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
|
|||
box.removeAll();
|
||||
}
|
||||
|
||||
@override
|
||||
bool addAllConnections(Iterable<Connection> 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<Connection, ExecError> getById(String id) {
|
||||
final result = memCache.getById(id);
|
||||
|
@ -109,9 +92,12 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
|
|||
}
|
||||
|
||||
@override
|
||||
bool updateConnection(Connection connection) {
|
||||
memCache.updateConnection(connection);
|
||||
box.put(connection);
|
||||
bool upsertConnection(Connection connection) {
|
||||
memCache.upsertConnection(connection);
|
||||
getById(connection.id).match(
|
||||
onSuccess: (existing) => box.put(connection.copy(obId: existing.obId)),
|
||||
onError: (_) => box.put(connection),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -121,7 +107,7 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
|
|||
return buildErrorResult(type: ErrorType.notFound);
|
||||
}
|
||||
|
||||
memCache.addConnection(connection);
|
||||
memCache.upsertConnection(connection);
|
||||
return Result.ok(connection);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<List<GroupData>, ExecError> getGroups() async {
|
||||
_logger.finest(() => 'Getting group (Mastodon List) data');
|
||||
final url = 'https://$serverName/api/v1/lists';
|
||||
|
@ -168,7 +167,66 @@ class GroupsClient extends FriendicaClient {
|
|||
: ExecError(type: ErrorType.localError, message: error.toString()));
|
||||
}
|
||||
|
||||
// TODO Convert groups for connection to using paging for real (if available)
|
||||
FutureResult<PagedResponse<List<Connection>>, 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<dynamic>)
|
||||
.map((json) => ConnectionMastodonExtensions.fromJson(json))
|
||||
.toList()))
|
||||
.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<GroupData, ExecError> 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<GroupData, ExecError> 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<bool, ExecError> 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<List<GroupData>, ExecError> getMemberGroupsForConnection(
|
||||
String connectionId) async {
|
||||
_logger.finest(() =>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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';
|
||||
|
||||
|
@ -24,7 +27,9 @@ 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;
|
||||
Logger.root.onRecord.listen((event) {
|
||||
final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||
|
@ -36,7 +41,10 @@ void main() async {
|
|||
await fixLetsEncryptCertOnOldAndroid();
|
||||
await dependencyInjectionInitialization();
|
||||
|
||||
runApp(const App());
|
||||
runApp(DevicePreview(
|
||||
enabled: !kReleaseMode && enablePreview,
|
||||
builder: (context) => const App(),
|
||||
));
|
||||
}
|
||||
|
||||
class App extends StatelessWidget {
|
||||
|
@ -45,6 +53,7 @@ class App extends StatelessWidget {
|
|||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsService = getIt<SettingsService>();
|
||||
return AnimatedBuilder(
|
||||
builder: (context, child) {
|
||||
return Portal(
|
||||
|
@ -102,9 +111,18 @@ class App extends StatelessWidget {
|
|||
)
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
theme: AppTheme.light,
|
||||
darkTheme: AppTheme.dark,
|
||||
themeMode: getIt<SettingsService>().themeMode,
|
||||
useInheritedMediaQuery: true,
|
||||
locale: DevicePreview.locale(context),
|
||||
builder: DevicePreview.appBuilder,
|
||||
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,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
Result<T, ExecError> buildErrorResult<T>({
|
||||
required ErrorType type,
|
||||
|
@ -14,22 +16,28 @@ Result<T, ExecError> buildErrorResult<T>({
|
|||
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<T, E> on Result<T, E> {
|
|||
|
||||
FutureResult<T, ExecError> execErrorCastAsync() async => execErrorCast();
|
||||
}
|
||||
|
||||
void logError(ExecError error, Logger logger,
|
||||
{Level logLevel = Level.INFO, String message = ''}) {
|
||||
logger.log(logLevel, '$message $error\n${error.trace}');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ 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';
|
||||
import 'screens/home.dart';
|
||||
import 'screens/interactions_viewer_screen.dart';
|
||||
import 'screens/message_thread_screen.dart';
|
||||
|
@ -34,6 +38,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 +125,28 @@ 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: 'add_users/:id',
|
||||
builder: (context, state) =>
|
||||
GroupAddUsersScreen(groupId: state.params['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: ScreenPaths.settings,
|
||||
name: ScreenPaths.settings,
|
||||
|
|
|
@ -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<ContactsScreen> {
|
|||
);
|
||||
late Widget body;
|
||||
if (contacts.isEmpty) {
|
||||
body = SingleChildScrollView(
|
||||
body = const SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Center(
|
||||
child: Text('No contacts'),
|
||||
|
@ -63,7 +64,14 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
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()),
|
||||
);
|
||||
},
|
||||
|
@ -73,7 +81,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
}
|
||||
|
||||
return Scaffold(
|
||||
drawer: StandardAppDrawer(skipPopDismiss: true),
|
||||
drawer: const StandardAppDrawer(skipPopDismiss: true),
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
|
@ -103,13 +111,20 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
|||
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(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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<EditorScreen> {
|
|||
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<EditorScreen> {
|
|||
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<EditorScreen> {
|
|||
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<EditorScreen> {
|
|||
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<EditorScreen> {
|
|||
if (mounted) {
|
||||
buildSnackbar(
|
||||
context, 'Error building link preview: $error');
|
||||
logError(error, _logger);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
188
lib/screens/group_add_users_screen.dart
Normal file
188
lib/screens/group_add_users_screen.dart
Normal file
|
@ -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<GroupAddUsersScreen> createState() => _GroupAddUsersScreenState();
|
||||
}
|
||||
|
||||
class _GroupAddUsersScreenState extends State<GroupAddUsersScreen> {
|
||||
static final _logger = Logger('$GroupAddUsersScreen');
|
||||
var filterText = '';
|
||||
late GroupData groupData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final manager =
|
||||
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value;
|
||||
groupData =
|
||||
manager.getMyGroups().where((g) => g.id == widget.groupId).first;
|
||||
}
|
||||
|
||||
Future<void> 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<NetworkStatusService>();
|
||||
final activeProfile = context.watch<AccountsService>();
|
||||
final manager = context
|
||||
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
75
lib/screens/group_create_screen.dart
Normal file
75
lib/screens/group_create_screen.dart
Normal file
|
@ -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<GroupCreateScreen> createState() => _GroupCreateScreenState();
|
||||
}
|
||||
|
||||
class _GroupCreateScreenState extends State<GroupCreateScreen> {
|
||||
final groupTextController = TextEditingController();
|
||||
|
||||
Future<void> 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<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
269
lib/screens/group_editor_screen.dart
Normal file
269
lib/screens/group_editor_screen.dart
Normal file
|
@ -0,0 +1,269 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
import '../controls/linear_status_indicator.dart';
|
||||
import '../controls/padding.dart';
|
||||
import '../controls/responsive_max_width.dart';
|
||||
import '../controls/standard_appbar.dart';
|
||||
import '../controls/status_and_refresh_button.dart';
|
||||
import '../globals.dart';
|
||||
import '../models/connection.dart';
|
||||
import '../models/group_data.dart';
|
||||
import '../routes.dart';
|
||||
import '../services/connections_manager.dart';
|
||||
import '../services/network_status_service.dart';
|
||||
import '../utils/active_profile_selector.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class GroupEditorScreen extends StatefulWidget {
|
||||
final String groupId;
|
||||
|
||||
const GroupEditorScreen({super.key, required this.groupId});
|
||||
|
||||
@override
|
||||
State<GroupEditorScreen> createState() => _GroupEditorScreenState();
|
||||
}
|
||||
|
||||
class _GroupEditorScreenState extends State<GroupEditorScreen> {
|
||||
final groupTextController = TextEditingController();
|
||||
var processingUpdate = false;
|
||||
var allowNameEditing = false;
|
||||
var filterText = '';
|
||||
late GroupData groupData;
|
||||
|
||||
Future<void> updateGroupName(
|
||||
BuildContext context, ConnectionsManager manager) async {
|
||||
processingUpdate = true;
|
||||
final updated = groupTextController.text;
|
||||
if (groupTextController.text != groupData.name) {
|
||||
final confirm = await showYesNoDialog(
|
||||
context, 'Change the group name from ${groupData.name} to $updated?');
|
||||
if (context.mounted && confirm == true) {
|
||||
await manager.renameGroup(widget.groupId, updated).match(
|
||||
onSuccess: (updatedGroupData) {
|
||||
groupData = updatedGroupData;
|
||||
setState(() {
|
||||
allowNameEditing = false;
|
||||
});
|
||||
}, onError: (error) {
|
||||
buildSnackbar(context, 'Error renaming group: $error');
|
||||
});
|
||||
} else {
|
||||
groupTextController.text = groupData.name;
|
||||
}
|
||||
}
|
||||
processingUpdate = false;
|
||||
}
|
||||
|
||||
Future<void> deleteGroup(ConnectionsManager manager) async {
|
||||
final confirm = await showYesNoDialog(context,
|
||||
"Permanently delete group ${groupData.name}? This can't be undone.");
|
||||
if (context.mounted && confirm == true) {
|
||||
await manager.deleteGroup(groupData).match(
|
||||
onSuccess: (_) => context.canPop() ? context.pop() : null,
|
||||
onError: (error) =>
|
||||
buildSnackbar(context, 'Error trying to delete group: $error'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeUserFromGroup(
|
||||
ConnectionsManager manager,
|
||||
Connection connection,
|
||||
) async {
|
||||
final messageBase = '${connection.name} from ${groupData.name}';
|
||||
final confirm = await showYesNoDialog(context, 'Remove $messageBase?');
|
||||
if (context.mounted && confirm == true) {
|
||||
final message =
|
||||
await manager.removeUserFromGroup(groupData, connection).fold(
|
||||
onSuccess: (_) => 'Removed $messageBase',
|
||||
onError: (error) => 'Error removing $messageBase: $error',
|
||||
);
|
||||
buildSnackbar(context, message);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final manager =
|
||||
getIt<ActiveProfileSelector<ConnectionsManager>>().activeEntry.value;
|
||||
groupData = manager
|
||||
.getMyGroups()
|
||||
.where(
|
||||
(g) => g.id == widget.groupId,
|
||||
)
|
||||
.first;
|
||||
manager.refreshGroupMemberships(groupData);
|
||||
groupTextController.text = groupData.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nss = getIt<NetworkStatusService>();
|
||||
final manager = context
|
||||
.watch<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
|
||||
final filterTextLC = filterText.toLowerCase();
|
||||
final members = manager
|
||||
.getGroupMembers(groupData)
|
||||
.transform((ms) => ms
|
||||
.where((m) =>
|
||||
filterText.isEmpty ||
|
||||
m.name.toLowerCase().contains(filterTextLC) ||
|
||||
m.handle.toLowerCase().contains(filterTextLC))
|
||||
.toList())
|
||||
.getValueOrElse(() => <Connection>[]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: StandardAppBar.build(
|
||||
context,
|
||||
'Group Editor',
|
||||
withHome: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => deleteGroup(manager),
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
manager.refreshGroups();
|
||||
},
|
||||
child: ResponsiveMaxWidth(
|
||||
child: Column(
|
||||
children: [
|
||||
StandardLinearProgressIndicator(nss.connectionUpdateStatus),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
enabled: allowNameEditing,
|
||||
readOnly: !allowNameEditing,
|
||||
onEditingComplete: () async {
|
||||
if (processingUpdate) {
|
||||
return;
|
||||
}
|
||||
updateGroupName(context, manager);
|
||||
},
|
||||
onTapOutside: (_) async {
|
||||
if (processingUpdate) {
|
||||
return;
|
||||
}
|
||||
updateGroupName(context, manager);
|
||||
},
|
||||
controller: groupTextController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Group Name',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: const BorderSide(),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (allowNameEditing) {
|
||||
groupTextController.text = groupData.name;
|
||||
}
|
||||
setState(() {
|
||||
allowNameEditing = !allowNameEditing;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const VerticalPadding(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Group Members:',
|
||||
style: Theme.of(context).textTheme.headlineSmall),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.push(
|
||||
'${ScreenPaths.groupManagement}/add_users/${widget.groupId}');
|
||||
},
|
||||
icon: const Icon(Icons.add)),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
filterText = value.toLowerCase();
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Filter By Name',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).highlightColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StatusAndRefreshButton(
|
||||
valueListenable: nss.connectionUpdateStatus,
|
||||
refreshFunction: () async =>
|
||||
manager.refreshGroupMemberships(groupData),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final m = members[index];
|
||||
return ListTile(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
59
lib/screens/group_management_screen.dart
Normal file
59
lib/screens/group_management_screen.dart
Normal file
|
@ -0,0 +1,59 @@
|
|||
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<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.activeEntry
|
||||
.value;
|
||||
final groups = manager.getMyGroups();
|
||||
groups.sort((g1, g2) => g1.name.compareTo(g2.name));
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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<ColorBlindnessType>(
|
||||
value: settings.colorBlindnessType,
|
||||
items: ColorBlindnessType.values
|
||||
.map((c) => DropdownMenuItem(value: c, child: Text(c.name)))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
settings.colorBlindnessType = value ?? ColorBlindnessType.none;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,16 +46,12 @@ extension DirectMessageFriendicaExtension on DirectMessage {
|
|||
.andThenSuccess((cm) {
|
||||
if (getIt<AccountsService>().currentProfile.userId != senderId) {
|
||||
final s = ConnectionFriendicaExtensions.fromJson(json['sender']);
|
||||
if (cm.getById(s.id).isFailure) {
|
||||
cm.addConnection(s);
|
||||
}
|
||||
cm.upsertConnection(s);
|
||||
}
|
||||
|
||||
if (getIt<AccountsService>().currentProfile.userId != recipientId) {
|
||||
final r = ConnectionFriendicaExtensions.fromJson(json['recipient']);
|
||||
if (cm.getById(r.id).isFailure) {
|
||||
cm.addConnection(r);
|
||||
}
|
||||
cm.upsertConnection(r);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ extension NotificationMastodonExtension on UserNotification {
|
|||
final from = ConnectionMastodonExtensions.fromJson(json['account']);
|
||||
getIt<ActiveProfileSelector<ConnectionsManager>>()
|
||||
.activeEntry
|
||||
.andThenSuccess((manager) => manager.addConnection(from));
|
||||
.andThenSuccess((manager) => manager.upsertConnection(from));
|
||||
var statusId = '';
|
||||
var statusLink = '';
|
||||
var content = '';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -22,20 +22,26 @@ class ConnectionsManager extends ChangeNotifier {
|
|||
|
||||
ConnectionsManager(this.conRepo, this.groupsRepo);
|
||||
|
||||
bool addConnection(Connection connection) {
|
||||
return conRepo.addConnection(connection);
|
||||
}
|
||||
|
||||
List<Connection> 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<Connection> newConnections) {
|
||||
return conRepo.addAllConnections(newConnections);
|
||||
bool upsertAllConnections(Iterable<Connection> newConnections) {
|
||||
newConnections.forEach(upsertConnection);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> 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) {
|
||||
|
@ -138,11 +144,13 @@ class ConnectionsManager extends ChangeNotifier {
|
|||
final results = <String, Connection>{};
|
||||
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);
|
||||
|
@ -161,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 {
|
||||
|
@ -178,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}');
|
||||
|
@ -194,6 +207,78 @@ class ConnectionsManager extends ChangeNotifier {
|
|||
return myGroups;
|
||||
}
|
||||
|
||||
Result<List<Connection>, ExecError> getGroupMembers(GroupData group) {
|
||||
return groupsRepo
|
||||
.getGroupMembers(group)
|
||||
.transform(
|
||||
(members) => members
|
||||
..sort((c1, c2) =>
|
||||
c1.name.toLowerCase().compareTo(c2.name.toLowerCase())),
|
||||
)
|
||||
.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<GroupData, ExecError> createGroup(String newName) async {
|
||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
||||
.createGroup(newName)
|
||||
.withResultAsync((newGroup) async {
|
||||
groupsRepo.upsertGroup(newGroup);
|
||||
notifyListeners();
|
||||
});
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<GroupData, ExecError> renameGroup(
|
||||
String id, String newName) async {
|
||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
||||
.renameGroup(id, newName)
|
||||
.withResultAsync((renamedGroup) async {
|
||||
groupsRepo.upsertGroup(renamedGroup);
|
||||
notifyListeners();
|
||||
});
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
FutureResult<bool, ExecError> deleteGroup(GroupData groupData) async {
|
||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
||||
.deleteGroup(groupData)
|
||||
.withResultAsync((_) async {
|
||||
groupsRepo.deleteGroup(groupData);
|
||||
notifyListeners();
|
||||
});
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
void refreshGroups() {
|
||||
_updateMyGroups(true);
|
||||
}
|
||||
|
||||
Future<void> refreshGroupMemberships(GroupData group) async {
|
||||
var page = PagingData(limit: 50);
|
||||
final client = GroupsClient(getIt<AccountsService>().currentProfile);
|
||||
final allResults = <Connection>{};
|
||||
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<List<GroupData>, ExecError> getGroupsForUser(String id) {
|
||||
final result = groupsRepo.getGroupsForUser(id);
|
||||
if (result.isSuccess) {
|
||||
|
@ -211,33 +296,31 @@ class ConnectionsManager extends ChangeNotifier {
|
|||
FutureResult<bool, ExecError> addUserToGroup(
|
||||
GroupData group, Connection connection) async {
|
||||
_logger.finest('Adding ${connection.name} to group: ${group.name}');
|
||||
final result = await GroupsClient(getIt<AccountsService>().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<AccountsService>().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<bool, ExecError> removeUserFromGroup(
|
||||
GroupData group, Connection connection) async {
|
||||
_logger.finest('Removing ${connection.name} from group: ${group.name}');
|
||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
||||
.removeConnectionFromGroup(group, connection);
|
||||
result.match(
|
||||
onSuccess: (_) => _refreshGroupListData(connection.id, true),
|
||||
onError: (error) {
|
||||
return GroupsClient(getIt<AccountsService>().currentProfile)
|
||||
.removeConnectionFromGroup(group, connection)
|
||||
.withResultAsync((_) async => refreshGroupMemberships(group))
|
||||
.withResult((_) => notifyListeners())
|
||||
.mapError(
|
||||
(error) {
|
||||
_logger.severe(
|
||||
'Error removing ${connection.name} from group: ${group.name}');
|
||||
return error;
|
||||
},
|
||||
);
|
||||
|
||||
return result.execErrorCast();
|
||||
}
|
||||
|
||||
Result<Connection, ExecError> getById(String id) {
|
||||
|
@ -298,7 +381,7 @@ class ConnectionsManager extends ChangeNotifier {
|
|||
.getConnectionWithStatus(connection)
|
||||
.match(
|
||||
onSuccess: (update) {
|
||||
updateConnection(update);
|
||||
upsertConnection(update);
|
||||
if (withNotification) {
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
@ -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<void> 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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
|||
|
||||
Future<void> buildSnackbar(BuildContext context, String message,
|
||||
{int durationSec = 3}) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final snackBar = SnackBar(
|
||||
content: SelectableText(message),
|
||||
duration: Duration(seconds: durationSec),
|
||||
|
|
186
pubspec.lock
186
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:
|
||||
|
@ -249,14 +257,22 @@ 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:
|
||||
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:
|
||||
|
@ -265,6 +281,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:
|
||||
|
@ -293,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:
|
||||
|
@ -358,6 +382,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:
|
||||
|
@ -431,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
|
||||
|
@ -456,6 +485,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:
|
||||
|
@ -508,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:
|
||||
|
@ -564,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:
|
||||
|
@ -588,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:
|
||||
|
@ -600,6 +637,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:
|
||||
|
@ -668,58 +713,66 @@ 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_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:
|
||||
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:
|
||||
|
@ -728,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:
|
||||
|
@ -828,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:
|
||||
|
@ -900,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:
|
||||
|
@ -947,11 +992,12 @@ packages:
|
|||
result_monad:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: result_monad
|
||||
sha256: "8f7720b9d517dbb54d612e2f6c6c4f409d51374f0d9ff9749dfcb0e0c6ab2fd4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
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:
|
||||
|
@ -988,18 +1034,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:
|
||||
|
@ -1089,12 +1135,12 @@ 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
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
|
@ -1201,18 +1247,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:
|
||||
|
@ -1225,10 +1271,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:
|
||||
|
@ -1289,10 +1335,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:
|
||||
|
@ -1321,18 +1367,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:
|
||||
|
|
23
pubspec.yaml
23
pubspec.yaml
|
@ -26,19 +26,21 @@ 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
|
||||
provider: ^6.0.4
|
||||
result_monad: ^2.0.2
|
||||
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
|
||||
|
@ -52,6 +54,9 @@ dependencies:
|
|||
carousel_slider: ^4.2.1
|
||||
device_info_plus: ^8.0.0
|
||||
string_validator: ^0.3.0
|
||||
device_preview: ^1.1.0
|
||||
color_blindness: ^0.1.2
|
||||
stack_trace: ^1.11.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <desktop_window/desktop_window_plugin.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue