mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 15:53: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
|
* 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.
|
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.
|
* 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
|
* Fixes
|
||||||
|
* Seemingly disappearing contacts on Contacts screen have been corrected.
|
||||||
* New Features
|
* New Features
|
||||||
* Responsive design for allowing the image and video attachments to scale up for larger videos
|
* 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
|
## 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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AppTheme {
|
const _seedColor = Colors.indigo;
|
||||||
static ThemeData light = ThemeData(
|
final _lightScheme = ColorScheme.fromSeed(
|
||||||
colorSchemeSeed: Colors.indigo,
|
seedColor: _seedColor,
|
||||||
useMaterial3: true,
|
brightness: Brightness.light,
|
||||||
);
|
);
|
||||||
|
|
||||||
static ThemeData dark = ThemeData(
|
final _darkScheme = ColorScheme.fromSeed(
|
||||||
colorSchemeSeed: Colors.indigo,
|
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,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,11 @@ class MediaKitAvControl extends StatefulWidget {
|
||||||
|
|
||||||
class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
||||||
static final _logger = Logger('$MediaKitAvControl');
|
static final _logger = Logger('$MediaKitAvControl');
|
||||||
final player = Player();
|
final Player player = Player(
|
||||||
|
configuration: const PlayerConfiguration(
|
||||||
|
logLevel: MPVLogLevel.warn,
|
||||||
|
),
|
||||||
|
);
|
||||||
VideoController? controller;
|
VideoController? controller;
|
||||||
var needToOpen = true;
|
var needToOpen = true;
|
||||||
|
|
||||||
|
@ -30,7 +34,7 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
||||||
super.initState();
|
super.initState();
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
_logger.info('initializing');
|
_logger.info('initializing');
|
||||||
controller = await VideoController.create(player.handle);
|
controller = await VideoController.create(player);
|
||||||
_logger.info('initialized');
|
_logger.info('initialized');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
@ -78,8 +82,17 @@ class _MediaKitAvControlState extends State<MediaKitAvControl> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
print('Building MediaKit Control');
|
print('Building MediaKit Control');
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
return const Center(
|
return Container(
|
||||||
child: CircularProgressIndicator(),
|
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:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:relatica/models/exec_error.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../../models/gallery_data.dart';
|
import '../../models/gallery_data.dart';
|
||||||
|
@ -55,10 +56,9 @@ class _MediaUploadsControlState extends State<MediaUploadsControl> {
|
||||||
.entryMediaItems.attachments
|
.entryMediaItems.attachments
|
||||||
.addAll(newEntries)),
|
.addAll(newEntries)),
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
if (mounted) {
|
buildSnackbar(context,
|
||||||
buildSnackbar(context,
|
'Error selecting attachments: $error');
|
||||||
'Error selecting attachments: $error');
|
logError(error, _logger);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.camera_alt),
|
icon: const Icon(Icons.camera_alt),
|
||||||
|
@ -111,7 +111,7 @@ class _MediaUploadsControlState extends State<MediaUploadsControl> {
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(context).backgroundColor,
|
color: Theme.of(context).colorScheme.background,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
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:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
import '../models/exec_error.dart';
|
||||||
import '../models/user_notification.dart';
|
import '../models/user_notification.dart';
|
||||||
import '../routes.dart';
|
import '../routes.dart';
|
||||||
import '../services/connections_manager.dart';
|
import '../services/connections_manager.dart';
|
||||||
|
@ -139,11 +141,11 @@ class NotificationControl extends StatelessWidget {
|
||||||
onPressed: manager == null
|
onPressed: manager == null
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
final result = await manager.markSeen(notification);
|
await manager.markSeen(notification).withError((error) {
|
||||||
if (result.isFailure) {
|
buildSnackbar(
|
||||||
buildSnackbar(context,
|
context, 'Error marking notification: $error');
|
||||||
'Error marking notification: ${result.error}');
|
logError(error, _logger);
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.close_rounded)),
|
icon: Icon(Icons.close_rounded)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -73,6 +73,12 @@ class StandardAppDrawer extends StatelessWidget {
|
||||||
'Direct Messages',
|
'Direct Messages',
|
||||||
() => context.pushNamed(ScreenPaths.messages),
|
() => context.pushNamed(ScreenPaths.messages),
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
buildMenuButton(
|
||||||
|
context,
|
||||||
|
'Groups Management',
|
||||||
|
() => context.pushNamed(ScreenPaths.groupManagement),
|
||||||
|
),
|
||||||
buildMenuButton(
|
buildMenuButton(
|
||||||
context,
|
context,
|
||||||
'Settings',
|
'Settings',
|
||||||
|
|
|
@ -167,6 +167,7 @@ class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
||||||
width: items.length > 1
|
width: items.length > 1
|
||||||
? ResponsiveSizesCalculator(context).maxThumbnailWidth
|
? ResponsiveSizesCalculator(context).maxThumbnailWidth
|
||||||
: ResponsiveSizesCalculator(context).viewPortalWidth,
|
: ResponsiveSizesCalculator(context).viewPortalWidth,
|
||||||
|
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) {
|
separatorBuilder: (context, index) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:relatica/models/exec_error.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../../globals.dart';
|
import '../../globals.dart';
|
||||||
|
@ -102,6 +103,7 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
});
|
});
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
buildSnackbar(context, 'Error resharing post by ${widget.entry.author}');
|
buildSnackbar(context, 'Error resharing post by ${widget.entry.author}');
|
||||||
|
logError(error, _logger);
|
||||||
});
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
|
@ -128,6 +130,7 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
buildSnackbar(
|
buildSnackbar(
|
||||||
context, 'Error un-resharing post by ${widget.entry.author}');
|
context, 'Error un-resharing post by ${widget.entry.author}');
|
||||||
|
logError(error, _logger);
|
||||||
});
|
});
|
||||||
setState(() {
|
setState(() {
|
||||||
isProcessing = false;
|
isProcessing = false;
|
||||||
|
|
|
@ -3,40 +3,18 @@ import 'package:result_monad/result_monad.dart';
|
||||||
import '../../models/connection.dart';
|
import '../../models/connection.dart';
|
||||||
import '../../models/exec_error.dart';
|
import '../../models/exec_error.dart';
|
||||||
|
|
||||||
class IConnectionsRepo {
|
abstract class IConnectionsRepo {
|
||||||
void clear() {
|
void clear();
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool addConnection(Connection connection) {
|
bool upsertConnection(Connection connection);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool addAllConnections(Iterable<Connection> newConnections) {
|
Result<Connection, ExecError> getById(String id);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool updateConnection(Connection connection) {
|
Result<Connection, ExecError> getByName(String name);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<Connection, ExecError> getById(String id) {
|
Result<Connection, ExecError> getByHandle(String handle);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<Connection, ExecError> getByName(String name) {
|
List<Connection> getMyContacts();
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<Connection, ExecError> getByHandle(String handle) {
|
List<Connection> getKnownUsersByName(String name);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Connection> getMyContacts() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Connection> getKnownUsersByName(String name) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
import '../../models/connection.dart';
|
||||||
import '../../models/exec_error.dart';
|
import '../../models/exec_error.dart';
|
||||||
import '../../models/group_data.dart';
|
import '../../models/group_data.dart';
|
||||||
|
|
||||||
class IGroupsRepo {
|
abstract class IGroupsRepo {
|
||||||
void addAllGroups(List<GroupData> groups) {
|
void addAllGroups(List<GroupData> groups);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearMyGroups() {
|
void addConnectionToGroup(GroupData group, Connection connection);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<GroupData> getMyGroups() {
|
void clearMyGroups();
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<List<GroupData>, ExecError> getGroupsForUser(String id) {
|
void upsertGroup(GroupData group);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool updateConnectionGroupData(String id, List<GroupData> currentGroups) {
|
void deleteGroup(GroupData group);
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
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();
|
_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
|
@override
|
||||||
List<Connection> getKnownUsersByName(String name) {
|
List<Connection> getKnownUsersByName(String name) {
|
||||||
return _connectionsByName.values.where((it) {
|
return _connectionsByName.values.where((it) {
|
||||||
|
@ -47,7 +28,7 @@ class MemoryConnectionsRepo implements IConnectionsRepo {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateConnection(Connection connection) {
|
bool upsertConnection(Connection connection) {
|
||||||
_connectionsById[connection.id] = connection;
|
_connectionsById[connection.id] = connection;
|
||||||
_connectionsByName[connection.name] = connection;
|
_connectionsByName[connection.name] = connection;
|
||||||
int index = _myContacts.indexWhere((c) => c.id == connection.id);
|
int index = _myContacts.indexWhere((c) => c.id == connection.id);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
import '../../models/connection.dart';
|
||||||
import '../../models/exec_error.dart';
|
import '../../models/exec_error.dart';
|
||||||
import '../../models/group_data.dart';
|
import '../../models/group_data.dart';
|
||||||
import '../interfaces/groups_repo.intf.dart';
|
import '../interfaces/groups_repo.intf.dart';
|
||||||
|
|
||||||
class MemoryGroupsRepo implements IGroupsRepo {
|
class MemoryGroupsRepo implements IGroupsRepo {
|
||||||
final _groupsForConnection = <String, List<GroupData>>{};
|
final _groupsForConnection = <String, List<GroupData>>{};
|
||||||
|
final _connectionsForGroup = <String, Set<Connection>>{};
|
||||||
final _myGroups = <GroupData>{};
|
final _myGroups = <GroupData>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -25,11 +27,28 @@ class MemoryGroupsRepo implements IGroupsRepo {
|
||||||
return _myGroups.toList();
|
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
|
@override
|
||||||
void clearMyGroups() {
|
void clearMyGroups() {
|
||||||
_myGroups.clear();
|
_myGroups.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addConnectionToGroup(GroupData group, Connection connection) {
|
||||||
|
_connectionsForGroup.putIfAbsent(group.id, () => {}).add(connection);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addAllGroups(List<GroupData> groups) {
|
void addAllGroups(List<GroupData> groups) {
|
||||||
_myGroups.addAll(groups);
|
_myGroups.addAll(groups);
|
||||||
|
@ -40,4 +59,20 @@ class MemoryGroupsRepo implements IGroupsRepo {
|
||||||
_groupsForConnection[id] = currentGroups;
|
_groupsForConnection[id] = currentGroups;
|
||||||
return true;
|
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();
|
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
|
@override
|
||||||
Result<Connection, ExecError> getById(String id) {
|
Result<Connection, ExecError> getById(String id) {
|
||||||
final result = memCache.getById(id);
|
final result = memCache.getById(id);
|
||||||
|
@ -109,9 +92,12 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateConnection(Connection connection) {
|
bool upsertConnection(Connection connection) {
|
||||||
memCache.updateConnection(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +107,7 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
|
||||||
return buildErrorResult(type: ErrorType.notFound);
|
return buildErrorResult(type: ErrorType.notFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
memCache.addConnection(connection);
|
memCache.upsertConnection(connection);
|
||||||
return Result.ok(connection);
|
return Result.ok(connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,7 +154,6 @@ class GroupsClient extends FriendicaClient {
|
||||||
|
|
||||||
GroupsClient(super.credentials) : super();
|
GroupsClient(super.credentials) : super();
|
||||||
|
|
||||||
// TODO Convert Groups to using paging for real (if it is supported)
|
|
||||||
FutureResult<List<GroupData>, ExecError> getGroups() async {
|
FutureResult<List<GroupData>, ExecError> getGroups() async {
|
||||||
_logger.finest(() => 'Getting group (Mastodon List) data');
|
_logger.finest(() => 'Getting group (Mastodon List) data');
|
||||||
final url = 'https://$serverName/api/v1/lists';
|
final url = 'https://$serverName/api/v1/lists';
|
||||||
|
@ -168,7 +167,66 @@ class GroupsClient extends FriendicaClient {
|
||||||
: ExecError(type: ErrorType.localError, message: error.toString()));
|
: 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(
|
FutureResult<List<GroupData>, ExecError> getMemberGroupsForConnection(
|
||||||
String connectionId) async {
|
String connectionId) async {
|
||||||
_logger.finest(() =>
|
_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:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -24,7 +27,9 @@ import 'utils/old_android_letsencrypte_cert.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
MediaKit.ensureInitialized();
|
||||||
// await dotenv.load(fileName: '.env');
|
// await dotenv.load(fileName: '.env');
|
||||||
|
const enablePreview = false;
|
||||||
Logger.root.level = Level.FINER;
|
Logger.root.level = Level.FINER;
|
||||||
Logger.root.onRecord.listen((event) {
|
Logger.root.onRecord.listen((event) {
|
||||||
final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName;
|
||||||
|
@ -36,7 +41,10 @@ void main() async {
|
||||||
await fixLetsEncryptCertOnOldAndroid();
|
await fixLetsEncryptCertOnOldAndroid();
|
||||||
await dependencyInjectionInitialization();
|
await dependencyInjectionInitialization();
|
||||||
|
|
||||||
runApp(const App());
|
runApp(DevicePreview(
|
||||||
|
enabled: !kReleaseMode && enablePreview,
|
||||||
|
builder: (context) => const App(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends StatelessWidget {
|
class App extends StatelessWidget {
|
||||||
|
@ -45,6 +53,7 @@ class App extends StatelessWidget {
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final settingsService = getIt<SettingsService>();
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Portal(
|
return Portal(
|
||||||
|
@ -102,9 +111,18 @@ class App extends StatelessWidget {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: MaterialApp.router(
|
child: MaterialApp.router(
|
||||||
theme: AppTheme.light,
|
useInheritedMediaQuery: true,
|
||||||
darkTheme: AppTheme.dark,
|
locale: DevicePreview.locale(context),
|
||||||
themeMode: getIt<SettingsService>().themeMode,
|
builder: DevicePreview.appBuilder,
|
||||||
|
theme: buildTheme(
|
||||||
|
brightness: Brightness.light,
|
||||||
|
blindnessType: settingsService.colorBlindnessType,
|
||||||
|
),
|
||||||
|
darkTheme: buildTheme(
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
blindnessType: settingsService.colorBlindnessType,
|
||||||
|
),
|
||||||
|
themeMode: settingsService.themeMode,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
scrollBehavior: AppScrollingBehavior(),
|
scrollBehavior: AppScrollingBehavior(),
|
||||||
routerDelegate: appRouter.routerDelegate,
|
routerDelegate: appRouter.routerDelegate,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
import 'package:stack_trace/stack_trace.dart';
|
||||||
|
|
||||||
Result<T, ExecError> buildErrorResult<T>({
|
Result<T, ExecError> buildErrorResult<T>({
|
||||||
required ErrorType type,
|
required ErrorType type,
|
||||||
|
@ -14,22 +16,28 @@ Result<T, ExecError> buildErrorResult<T>({
|
||||||
class ExecError {
|
class ExecError {
|
||||||
final ErrorType type;
|
final ErrorType type;
|
||||||
final String message;
|
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({
|
ExecError copy({
|
||||||
ErrorType? type,
|
ErrorType? type,
|
||||||
String? message,
|
String? message,
|
||||||
|
Trace? trace,
|
||||||
}) =>
|
}) =>
|
||||||
ExecError(
|
ExecError(
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
message: message ?? this.message,
|
message: message ?? this.message,
|
||||||
|
trace: trace ?? this.trace,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ExecError{type: $type, message: $message}';
|
return 'ExecError{type: $type, message: $message}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String printStackTrace() => trace.terse.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ErrorType {
|
enum ErrorType {
|
||||||
|
@ -50,3 +58,8 @@ extension ExecErrorExtension<T, E> on Result<T, E> {
|
||||||
|
|
||||||
FutureResult<T, ExecError> execErrorCastAsync() async => execErrorCast();
|
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;
|
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;
|
final String name;
|
||||||
|
|
||||||
GroupData(this.id, this.name);
|
GroupData(this.id, this.name);
|
||||||
|
@ -21,4 +11,12 @@ class GroupData {
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'GroupData{id: $id, name: $name}';
|
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/follow_request_adjudication_screen.dart';
|
||||||
import 'screens/gallery_browsers_screen.dart';
|
import 'screens/gallery_browsers_screen.dart';
|
||||||
import 'screens/gallery_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/home.dart';
|
||||||
import 'screens/interactions_viewer_screen.dart';
|
import 'screens/interactions_viewer_screen.dart';
|
||||||
import 'screens/message_thread_screen.dart';
|
import 'screens/message_thread_screen.dart';
|
||||||
|
@ -34,6 +38,7 @@ class ScreenPaths {
|
||||||
static String notifications = '/notifications';
|
static String notifications = '/notifications';
|
||||||
static String signin = '/signin';
|
static String signin = '/signin';
|
||||||
static String manageProfiles = '/switchProfiles';
|
static String manageProfiles = '/switchProfiles';
|
||||||
|
static String groupManagement = '/group_management';
|
||||||
static String signup = '/signup';
|
static String signup = '/signup';
|
||||||
static String userProfile = '/user_profile';
|
static String userProfile = '/user_profile';
|
||||||
static String userPosts = '/user_posts';
|
static String userPosts = '/user_posts';
|
||||||
|
@ -120,6 +125,28 @@ final appRouter = GoRouter(
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
MessageThreadScreen(parentThreadId: state.queryParams['uri']!),
|
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(
|
GoRoute(
|
||||||
path: ScreenPaths.settings,
|
path: ScreenPaths.settings,
|
||||||
name: ScreenPaths.settings,
|
name: ScreenPaths.settings,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import '../controls/current_profile_button.dart';
|
||||||
import '../controls/linear_status_indicator.dart';
|
import '../controls/linear_status_indicator.dart';
|
||||||
import '../controls/responsive_max_width.dart';
|
import '../controls/responsive_max_width.dart';
|
||||||
import '../controls/standard_app_drawer.dart';
|
import '../controls/standard_app_drawer.dart';
|
||||||
|
import '../controls/status_and_refresh_button.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
import '../models/connection.dart';
|
import '../models/connection.dart';
|
||||||
import '../routes.dart';
|
import '../routes.dart';
|
||||||
|
@ -47,7 +48,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
);
|
);
|
||||||
late Widget body;
|
late Widget body;
|
||||||
if (contacts.isEmpty) {
|
if (contacts.isEmpty) {
|
||||||
body = SingleChildScrollView(
|
body = const SingleChildScrollView(
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('No contacts'),
|
child: Text('No contacts'),
|
||||||
|
@ -63,7 +64,14 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
context.pushNamed(ScreenPaths.userProfile,
|
context.pushNamed(ScreenPaths.userProfile,
|
||||||
params: {'id': contact.id});
|
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()),
|
trailing: Text(contact.status.label()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -73,7 +81,7 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: StandardAppDrawer(skipPopDismiss: true),
|
drawer: const StandardAppDrawer(skipPopDismiss: true),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
@ -103,13 +111,20 @@ class _ContactsScreenState extends State<ContactsScreen> {
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(context).backgroundColor,
|
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.updateAllContacts(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
import '../controls/autocomplete/hashtag_autocomplete_options.dart';
|
import '../controls/autocomplete/hashtag_autocomplete_options.dart';
|
||||||
|
@ -15,6 +16,7 @@ import '../controls/padding.dart';
|
||||||
import '../controls/standard_appbar.dart';
|
import '../controls/standard_appbar.dart';
|
||||||
import '../controls/timeline/status_header_control.dart';
|
import '../controls/timeline/status_header_control.dart';
|
||||||
import '../globals.dart';
|
import '../globals.dart';
|
||||||
|
import '../models/exec_error.dart';
|
||||||
import '../models/group_data.dart';
|
import '../models/group_data.dart';
|
||||||
import '../models/image_entry.dart';
|
import '../models/image_entry.dart';
|
||||||
import '../models/link_preview_data.dart';
|
import '../models/link_preview_data.dart';
|
||||||
|
@ -110,13 +112,8 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
loaded = true;
|
loaded = true;
|
||||||
});
|
});
|
||||||
}, onError: (error) {
|
}, onError: (error) {
|
||||||
if (context.mounted) {
|
buildSnackbar(context, 'Error getting post for editing: $error');
|
||||||
buildSnackbar(
|
logError(error, _logger);
|
||||||
context,
|
|
||||||
'Error getting post for editing: $error',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_logger.severe('Error getting post for editing: $error');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,20 +149,25 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await manager.createNewStatus(
|
final result = await manager
|
||||||
|
.createNewStatus(
|
||||||
bodyText,
|
bodyText,
|
||||||
spoilerText: spoilerController.text,
|
spoilerText: spoilerController.text,
|
||||||
inReplyToId: widget.parentId,
|
inReplyToId: widget.parentId,
|
||||||
newMediaItems: newMediaItems,
|
newMediaItems: newMediaItems,
|
||||||
existingMediaItems: existingMediaItems,
|
existingMediaItems: existingMediaItems,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
);
|
)
|
||||||
|
.withError((error) {
|
||||||
|
buildSnackbar(context, 'Error posting: $error');
|
||||||
|
logError(error, _logger);
|
||||||
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isSubmitting = false;
|
isSubmitting = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
buildSnackbar(context, 'Error posting: ${result.error}');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +186,8 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
final result = await manager.editStatus(
|
final result = await manager
|
||||||
|
.editStatus(
|
||||||
widget.id,
|
widget.id,
|
||||||
bodyText,
|
bodyText,
|
||||||
spoilerText: spoilerController.text,
|
spoilerText: spoilerController.text,
|
||||||
|
@ -192,13 +195,17 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
newMediaItems: newMediaItems,
|
newMediaItems: newMediaItems,
|
||||||
existingMediaItems: existingMediaItems,
|
existingMediaItems: existingMediaItems,
|
||||||
newMediaItemVisibility: visibility,
|
newMediaItemVisibility: visibility,
|
||||||
);
|
)
|
||||||
|
.withError((error) {
|
||||||
|
buildSnackbar(context, 'Error updating $statusType: $error');
|
||||||
|
logError(error, _logger);
|
||||||
|
});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
isSubmitting = false;
|
isSubmitting = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
buildSnackbar(context, 'Error Updating $statusType: ${result.error}');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,6 +475,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
buildSnackbar(
|
buildSnackbar(
|
||||||
context, 'Error building link preview: $error');
|
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) {
|
Widget buildBody(BuildContext context) {
|
||||||
final border = OutlineInputBorder(
|
final border = OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(context).backgroundColor,
|
color: Theme.of(context).colorScheme.background,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -20,6 +22,7 @@ class SettingsScreen extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
buildLowBandwidthWidget(settings),
|
buildLowBandwidthWidget(settings),
|
||||||
buildThemeWidget(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) {
|
.andThenSuccess((cm) {
|
||||||
if (getIt<AccountsService>().currentProfile.userId != senderId) {
|
if (getIt<AccountsService>().currentProfile.userId != senderId) {
|
||||||
final s = ConnectionFriendicaExtensions.fromJson(json['sender']);
|
final s = ConnectionFriendicaExtensions.fromJson(json['sender']);
|
||||||
if (cm.getById(s.id).isFailure) {
|
cm.upsertConnection(s);
|
||||||
cm.addConnection(s);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getIt<AccountsService>().currentProfile.userId != recipientId) {
|
if (getIt<AccountsService>().currentProfile.userId != recipientId) {
|
||||||
final r = ConnectionFriendicaExtensions.fromJson(json['recipient']);
|
final r = ConnectionFriendicaExtensions.fromJson(json['recipient']);
|
||||||
if (cm.getById(r.id).isFailure) {
|
cm.upsertConnection(r);
|
||||||
cm.addConnection(r);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ extension NotificationMastodonExtension on UserNotification {
|
||||||
final from = ConnectionMastodonExtensions.fromJson(json['account']);
|
final from = ConnectionMastodonExtensions.fromJson(json['account']);
|
||||||
getIt<ActiveProfileSelector<ConnectionsManager>>()
|
getIt<ActiveProfileSelector<ConnectionsManager>>()
|
||||||
.activeEntry
|
.activeEntry
|
||||||
.andThenSuccess((manager) => manager.addConnection(from));
|
.andThenSuccess((manager) => manager.upsertConnection(from));
|
||||||
var statusId = '';
|
var statusId = '';
|
||||||
var statusLink = '';
|
var statusLink = '';
|
||||||
var content = '';
|
var content = '';
|
||||||
|
|
|
@ -72,14 +72,14 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
final connection = ConnectionMastodonExtensions.fromJson(json['account']);
|
final connection = ConnectionMastodonExtensions.fromJson(json['account']);
|
||||||
connectionManager?.addConnection(connection);
|
connectionManager?.upsertConnection(connection);
|
||||||
|
|
||||||
late final String reshareAuthor;
|
late final String reshareAuthor;
|
||||||
late final String reshareAuthorId;
|
late final String reshareAuthorId;
|
||||||
if (json['reblog'] != null) {
|
if (json['reblog'] != null) {
|
||||||
final rebloggedUser =
|
final rebloggedUser =
|
||||||
ConnectionMastodonExtensions.fromJson(json['reblog']['account']);
|
ConnectionMastodonExtensions.fromJson(json['reblog']['account']);
|
||||||
connectionManager?.addConnection(rebloggedUser);
|
connectionManager?.upsertConnection(rebloggedUser);
|
||||||
reshareAuthor = rebloggedUser.name;
|
reshareAuthor = rebloggedUser.name;
|
||||||
reshareAuthorId = rebloggedUser.id;
|
reshareAuthorId = rebloggedUser.id;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -22,20 +22,26 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
|
|
||||||
ConnectionsManager(this.conRepo, this.groupsRepo);
|
ConnectionsManager(this.conRepo, this.groupsRepo);
|
||||||
|
|
||||||
bool addConnection(Connection connection) {
|
|
||||||
return conRepo.addConnection(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Connection> getKnownUsersByName(String name) {
|
List<Connection> getKnownUsersByName(String name) {
|
||||||
return conRepo.getKnownUsersByName(name);
|
return conRepo.getKnownUsersByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool updateConnection(Connection connection) {
|
bool upsertConnection(Connection connection) {
|
||||||
return conRepo.updateConnection(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) {
|
bool upsertAllConnections(Iterable<Connection> newConnections) {
|
||||||
return conRepo.addAllConnections(newConnections);
|
newConnections.forEach(upsertConnection);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> acceptFollowRequest(Connection connection) async {
|
Future<void> acceptFollowRequest(Connection connection) async {
|
||||||
|
@ -47,7 +53,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
_logger
|
_logger
|
||||||
.finest('Successfully followed ${update.name}: ${update.status}');
|
.finest('Successfully followed ${update.name}: ${update.status}');
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
@ -65,7 +71,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
_logger
|
_logger
|
||||||
.finest('Successfully followed ${update.name}: ${update.status}');
|
.finest('Successfully followed ${update.name}: ${update.status}');
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
@ -83,7 +89,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
_logger
|
_logger
|
||||||
.finest('Successfully followed ${update.name}: ${update.status}');
|
.finest('Successfully followed ${update.name}: ${update.status}');
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
@ -101,7 +107,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
_logger
|
_logger
|
||||||
.finest('Successfully followed ${update.name}: ${update.status}');
|
.finest('Successfully followed ${update.name}: ${update.status}');
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
@ -119,7 +125,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
_logger
|
_logger
|
||||||
.finest('Successfully unfollowed ${update.name}: ${update.status}');
|
.finest('Successfully unfollowed ${update.name}: ${update.status}');
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
@ -138,11 +144,13 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
final results = <String, Connection>{};
|
final results = <String, Connection>{};
|
||||||
var moreResults = true;
|
var moreResults = true;
|
||||||
var maxId = -1;
|
var maxId = -1;
|
||||||
const limit = 200;
|
const limit = 50;
|
||||||
var currentPage = PagingData(limit: limit);
|
var currentPage = PagingData(limit: limit);
|
||||||
|
final originalContacts = conRepo.getMyContacts().toSet();
|
||||||
while (moreResults) {
|
while (moreResults) {
|
||||||
await client.getMyFollowers(currentPage).match(onSuccess: (followers) {
|
await client.getMyFollowers(currentPage).match(onSuccess: (followers) {
|
||||||
for (final f in followers.data) {
|
for (final f in followers.data) {
|
||||||
|
originalContacts.remove(f);
|
||||||
results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou);
|
results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou);
|
||||||
int id = int.parse(f.id);
|
int id = int.parse(f.id);
|
||||||
maxId = max(maxId, id);
|
maxId = max(maxId, id);
|
||||||
|
@ -161,6 +169,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
while (moreResults) {
|
while (moreResults) {
|
||||||
await client.getMyFollowing(currentPage).match(onSuccess: (following) {
|
await client.getMyFollowing(currentPage).match(onSuccess: (following) {
|
||||||
for (final f in following.data) {
|
for (final f in following.data) {
|
||||||
|
originalContacts.remove(f);
|
||||||
if (results.containsKey(f.id)) {
|
if (results.containsKey(f.id)) {
|
||||||
results[f.id] = f.copy(status: ConnectionStatus.mutual);
|
results[f.id] = f.copy(status: ConnectionStatus.mutual);
|
||||||
} else {
|
} 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();
|
final myContacts = conRepo.getMyContacts().toList();
|
||||||
myContacts.sort((c1, c2) => c1.name.compareTo(c2.name));
|
myContacts.sort((c1, c2) => c1.name.compareTo(c2.name));
|
||||||
_logger.fine('# Contacts:${myContacts.length}');
|
_logger.fine('# Contacts:${myContacts.length}');
|
||||||
|
@ -194,6 +207,78 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
return myGroups;
|
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) {
|
Result<List<GroupData>, ExecError> getGroupsForUser(String id) {
|
||||||
final result = groupsRepo.getGroupsForUser(id);
|
final result = groupsRepo.getGroupsForUser(id);
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
|
@ -211,33 +296,31 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
FutureResult<bool, ExecError> addUserToGroup(
|
FutureResult<bool, ExecError> addUserToGroup(
|
||||||
GroupData group, Connection connection) async {
|
GroupData group, Connection connection) async {
|
||||||
_logger.finest('Adding ${connection.name} to group: ${group.name}');
|
_logger.finest('Adding ${connection.name} to group: ${group.name}');
|
||||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
return await GroupsClient(getIt<AccountsService>().currentProfile)
|
||||||
.addConnectionToGroup(group, connection);
|
.addConnectionToGroup(group, connection)
|
||||||
result.match(
|
.withResultAsync((_) async => refreshGroupMemberships(group))
|
||||||
onSuccess: (_) => _refreshGroupListData(connection.id, true),
|
.withResult((_) => notifyListeners())
|
||||||
onError: (error) {
|
.mapError((error) {
|
||||||
_logger
|
_logger
|
||||||
.severe('Error adding ${connection.name} to group: ${group.name}');
|
.severe('Error adding ${connection.name} from group: ${group.name}');
|
||||||
},
|
return error;
|
||||||
);
|
});
|
||||||
|
|
||||||
return result.execErrorCast();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<bool, ExecError> removeUserFromGroup(
|
FutureResult<bool, ExecError> removeUserFromGroup(
|
||||||
GroupData group, Connection connection) async {
|
GroupData group, Connection connection) async {
|
||||||
_logger.finest('Removing ${connection.name} from group: ${group.name}');
|
_logger.finest('Removing ${connection.name} from group: ${group.name}');
|
||||||
final result = await GroupsClient(getIt<AccountsService>().currentProfile)
|
return GroupsClient(getIt<AccountsService>().currentProfile)
|
||||||
.removeConnectionFromGroup(group, connection);
|
.removeConnectionFromGroup(group, connection)
|
||||||
result.match(
|
.withResultAsync((_) async => refreshGroupMemberships(group))
|
||||||
onSuccess: (_) => _refreshGroupListData(connection.id, true),
|
.withResult((_) => notifyListeners())
|
||||||
onError: (error) {
|
.mapError(
|
||||||
|
(error) {
|
||||||
_logger.severe(
|
_logger.severe(
|
||||||
'Error removing ${connection.name} from group: ${group.name}');
|
'Error removing ${connection.name} from group: ${group.name}');
|
||||||
|
return error;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.execErrorCast();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Connection, ExecError> getById(String id) {
|
Result<Connection, ExecError> getById(String id) {
|
||||||
|
@ -298,7 +381,7 @@ class ConnectionsManager extends ChangeNotifier {
|
||||||
.getConnectionWithStatus(connection)
|
.getConnectionWithStatus(connection)
|
||||||
.match(
|
.match(
|
||||||
onSuccess: (update) {
|
onSuccess: (update) {
|
||||||
updateConnection(update);
|
upsertConnection(update);
|
||||||
if (withNotification) {
|
if (withNotification) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:color_blindness/color_blindness.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
@ -29,6 +30,16 @@ class SettingsService extends ChangeNotifier {
|
||||||
notifyListeners();
|
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 {
|
Future<void> initialize() async {
|
||||||
if (_initialized) {
|
if (_initialized) {
|
||||||
return;
|
return;
|
||||||
|
@ -36,9 +47,22 @@ class SettingsService extends ChangeNotifier {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
_lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false;
|
_lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false;
|
||||||
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
_themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey));
|
||||||
|
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _lowBandwidthModeKey = 'LowBandwidthMode';
|
const _lowBandwidthModeKey = 'LowBandwidthMode';
|
||||||
const _themeModeKey = 'ThemeMode';
|
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,
|
Future<void> buildSnackbar(BuildContext context, String message,
|
||||||
{int durationSec = 3}) async {
|
{int durationSec = 3}) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
content: SelectableText(message),
|
content: SelectableText(message),
|
||||||
duration: Duration(seconds: durationSec),
|
duration: Duration(seconds: durationSec),
|
||||||
|
|
186
pubspec.lock
186
pubspec.lock
|
@ -21,10 +21,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
|
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.6"
|
version: "3.3.7"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -185,6 +185,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
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:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -249,14 +257,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.0"
|
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:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95"
|
sha256: "435383ca05f212760b0a70426b5a90354fe6bd65992b3a5e27ab6ede74c02f5c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.0"
|
version: "8.2.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -265,6 +281,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.0"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -293,10 +317,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "0d923fb610d0abf67f2149c3a50ef85f78bebecfc4d645719ca70bcf4abc788f"
|
sha256: dcde5ad1a0cebcf3715ea3f24d0db1888bf77027a26c77d7779e8ef63b8ade62
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.7"
|
version: "5.2.9"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -358,6 +382,11 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -431,18 +460,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_web_auth_2
|
name: flutter_web_auth_2
|
||||||
sha256: "6aebfb1797bb1dd38cd32753832670482792e4f0b9cef0329d357f889dbf07c9"
|
sha256: "354002de1cf644b98af9b1b8c7a0f50f55d738667a54786ae4197e6ff87b224a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
flutter_web_auth_2_platform_interface:
|
flutter_web_auth_2_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_web_auth_2_platform_interface
|
name: flutter_web_auth_2_platform_interface
|
||||||
sha256: dd934033564cacff127b4776798dc2b27b2f2ebfd6b669746455b91c3611cfde
|
sha256: "91ff7f0bf4ca530aabf857433db2fbcc3d1b8e3c5347ecf58e5ace8f9d29edb0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.3"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -456,6 +485,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.0"
|
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:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -508,10 +545,10 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: feab99a20fd248c658c923ba98f4449ca6e575c3dee9fdf07146f4f33482c6bc
|
sha256: "99c7fbd4f73da5268046374576af2f6fef970088038d626c5c689a359479af40"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.5"
|
version: "6.5.7"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -564,18 +601,18 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker
|
name: image_picker
|
||||||
sha256: cb25f04595a88450970dbe727243ba8cd21b6f7e0d7d1fc5b789fc6f52e95494
|
sha256: f202f5d730eb8219e35e80c4461fb3a779940ad30ce8fde1586df756e3af25e6
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.7+1"
|
version: "0.8.7+3"
|
||||||
image_picker_android:
|
image_picker_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: dfb5b0f28b8786fcc662b7ed42bfb4b82a6cbbd74da1958384b10d40bdf212a7
|
sha256: "1ea6870350f56af8dab716459bd9d5dc76947e29e07a2ba1d0c172eaaf4f269c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.6+6"
|
version: "0.8.6+7"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -588,10 +625,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: d4cb8ab04f770dab9d04c7959e5f6d22e8c5280343d425f9344f93832cf58445
|
sha256: a1546ff5861fc15812953d4733b520c3d371cec3d2859a001ff04c46c4d81883
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.7+2"
|
version: "0.8.7+3"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -600,6 +637,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.3"
|
version: "2.6.3"
|
||||||
|
intl:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.17.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -668,58 +713,66 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit
|
name: media_kit
|
||||||
sha256: defc249a792bf39346e6ee3ec40bbe48685c54aab86a25c4c27cc54a99afb6b1
|
sha256: "4c2b3bb600c063ad194934ea7439bce3135cc6b4f9555222c3dc12ca1aa1d85c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
media_kit_libs_ios_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_ios_video
|
name: media_kit_libs_ios_video
|
||||||
sha256: a6ee06d466f539d161ef3b0c13f101713fd051d90c435a503420cb6a6f4c6e66
|
sha256: "28c6ddd5ed43263641293832c8d1fb3f24af81b4eba0b61d6da9bedadbf2e1b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.4"
|
||||||
media_kit_libs_linux:
|
media_kit_libs_linux:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_linux
|
name: media_kit_libs_linux
|
||||||
sha256: "7310b17dd2abc2e7363f78a273086445c2216c7b6dfb60933ca3814031d03814"
|
sha256: "21acc71cbae3518b3aeef9023a6a3a3decb579a40153764333814987ccd61040"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
media_kit_libs_macos_video:
|
media_kit_libs_macos_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_macos_video
|
name: media_kit_libs_macos_video
|
||||||
sha256: b3259875e201ec66d98ed33b32687ff14fcce7d91e74d420733c039556dff8cd
|
sha256: ab1cbdf51400e30a9087bd7d6e10c6130d17296e76313fedd0ef0c57dae8c0f4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.4"
|
||||||
media_kit_libs_windows_video:
|
media_kit_libs_windows_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_libs_windows_video
|
name: media_kit_libs_windows_video
|
||||||
sha256: c2bcbe31e6e6f1e6ae5813c5fe9b7bfde1110c2e95643869919972a7845948e1
|
sha256: "99a3a85b185ae012a8e3bd596cf0ca425834477d0bec86207548d6ff7c926254"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
media_kit_native_event_loop:
|
media_kit_native_event_loop:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_native_event_loop
|
name: media_kit_native_event_loop
|
||||||
sha256: "677ea41d13a2013ca7fe050674eac3b5d891185d4300779727a5860a85cdda60"
|
sha256: ed87140ad4b64156b2b470c8105f48d8cad7923c952ca05d23e02d28978d2cb3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.3"
|
||||||
media_kit_video:
|
media_kit_video:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: media_kit_video
|
name: media_kit_video
|
||||||
sha256: "1a870a3d731a9ce34e12ec5baed05b3081164f0c85b71baf05900ca47ee5639c"
|
sha256: "3860b1e8b2779a5702ecea7e6e3d8b16a79407ad478234008550cca660ac52ad"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.4"
|
version: "0.0.6"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -728,14 +781,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
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:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -828,18 +873,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7"
|
sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.24"
|
version: "2.0.25"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7"
|
sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -900,10 +945,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pointycastle
|
name: pointycastle
|
||||||
sha256: c3120a968135aead39699267f4c74bc9a08e4e909e86bc1b0af5bfd78691123c
|
sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.7.2"
|
version: "3.7.3"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -947,11 +992,12 @@ packages:
|
||||||
result_monad:
|
result_monad:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: result_monad
|
path: "."
|
||||||
sha256: "8f7720b9d517dbb54d612e2f6c6c4f409d51374f0d9ff9749dfcb0e0c6ab2fd4"
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "12a2ae1e0830f4aff32f1d94835901de545cf917"
|
||||||
source: hosted
|
url: "https://gitlab.com/HankG/dart-result-monad.git"
|
||||||
version: "2.0.2"
|
source: git
|
||||||
|
version: "2.3.0"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -988,18 +1034,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6"
|
sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.2"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603
|
sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1089,12 +1135,12 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqlite3
|
name: sqlite3
|
||||||
sha256: "822d321a008e194d7929357e5b58d2e4a04ab670d137182f9759152aa33180ff"
|
sha256: a3ba4b66a7ab170ce7aa3f5ac43c19ee8d6637afbe7b7c95c94112b4f4d91566
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.11.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||||
|
@ -1201,18 +1247,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8
|
sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.26"
|
version: "6.0.27"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92"
|
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.3"
|
version: "6.1.4"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1225,10 +1271,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a"
|
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.4"
|
version: "3.0.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1289,10 +1335,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
sha256: af308d08c672d5ff718c60127665249617c37a709cb8f0a18dd28a0360299b7c
|
sha256: "75c6d68cd479a25f34d635149ba6887bc8f1b2b2921841121fd44ea0c5bc1927"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.4.4"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1321,18 +1367,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.4.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.4"
|
||||||
window_to_front:
|
window_to_front:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
23
pubspec.yaml
23
pubspec.yaml
|
@ -26,19 +26,21 @@ dependencies:
|
||||||
image_picker: ^0.8.6
|
image_picker: ^0.8.6
|
||||||
logging: ^1.1.0
|
logging: ^1.1.0
|
||||||
markdown: ^7.0.1
|
markdown: ^7.0.1
|
||||||
media_kit: ^0.0.3
|
media_kit: ^0.0.5 # Primary package.
|
||||||
media_kit_libs_ios_video: ^1.0.2
|
media_kit_video: ^0.0.6 # For video rendering.
|
||||||
media_kit_libs_linux: ^1.0.1
|
media_kit_native_event_loop: ^1.0.3 # Support for higher number of concurrent instances & better performance.
|
||||||
media_kit_libs_macos_video: ^1.0.2
|
media_kit_libs_windows_video: ^1.0.2 # Windows package for video native libraries.
|
||||||
media_kit_libs_windows_video: ^1.0.1
|
media_kit_libs_android_video: ^1.0.0 # Android package for video native libraries.
|
||||||
media_kit_native_event_loop: ^1.0.2
|
media_kit_libs_macos_video: ^1.0.4 # macOS package for video native libraries.
|
||||||
media_kit_video: ^0.0.4
|
media_kit_libs_ios_video: ^1.0.4 # iOS package for video native libraries.
|
||||||
metadata_fetch: ^0.4.1
|
media_kit_libs_linux: ^1.0.2 # GNU/Linux dependency package. metadata_fetch: ^0.4.1
|
||||||
multi_trigger_autocomplete: ^0.1.1
|
multi_trigger_autocomplete: ^0.1.1
|
||||||
network_to_file_image: ^4.0.1
|
network_to_file_image: ^4.0.1
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
provider: ^6.0.4
|
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
|
scrollable_positioned_list: ^0.3.5
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.0.15
|
||||||
sqlite3: ^1.9.1
|
sqlite3: ^1.9.1
|
||||||
|
@ -52,6 +54,9 @@ dependencies:
|
||||||
carousel_slider: ^4.2.1
|
carousel_slider: ^4.2.1
|
||||||
device_info_plus: ^8.0.0
|
device_info_plus: ^8.0.0
|
||||||
string_validator: ^0.3.0
|
string_validator: ^0.3.0
|
||||||
|
device_preview: ^1.1.0
|
||||||
|
color_blindness: ^0.1.2
|
||||||
|
stack_trace: ^1.11.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <desktop_window/desktop_window_plugin.h>
|
#include <desktop_window/desktop_window_plugin.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_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 <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
|
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi"));
|
||||||
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
MediaKitVideoPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
|
ObjectboxFlutterLibsPluginRegisterWithRegistrar(
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_window
|
desktop_window
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
objectbox_flutter_libs
|
objectbox_flutter_libs
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
@ -12,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
media_kit_libs_windows_video
|
|
||||||
media_kit_native_event_loop
|
media_kit_native_event_loop
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue