1
0
Fork 0
forked from Nexus/nexus

Compare commits

...

15 commits

29 changed files with 824 additions and 591 deletions

View file

@ -261,12 +261,12 @@ class ClientController extends AsyncNotifier<int> {
}
}
Future<String?> discoverHomeserver(Uri homeserver) async {
Future<Uri?> discoverHomeserver(Uri homeserver) async {
try {
final response = await _sendCommand("discover_homeserver", {
"user_id": "@fake-user:${homeserver.host}",
});
return response["m.homeserver"]?["base_url"];
return Uri.parse(response["m.homeserver"]?["base_url"]);
} catch (error) {
return null;
}

View file

@ -24,7 +24,7 @@ class MembersController extends AsyncNotifier<ISet<Event>> {
.getRoomState(
GetRoomStateRequest(
roomId: roomId,
fetchMembers: room.metadata?.hasMemberList ?? true,
fetchMembers: !(room.metadata?.hasMemberList ?? false),
includeMembers: true,
),
);

View file

@ -0,0 +1,64 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/members_by_status_controller.dart";
import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/models/configs/members_by_status_config.dart";
import "package:nexus/models/content/content.dart";
import "package:nexus/models/content/create.dart";
import "package:nexus/models/content/power_levels.dart";
import "package:nexus/models/event.dart";
class MembersGroupedController extends AsyncNotifier<IMap<int?, ISet<Event>>> {
final MembersByStatusConfig config;
MembersGroupedController(this.config);
@override
Future<IMap<int?, ISet<Event>>> build() async {
final room = ref.watch(
RoomsController.provider.select((value) => value[config.roomId]),
);
final createRowId = room?.state[EventType.create.type]?[""];
final createEvent = createRowId == null ? null : room?.events[createRowId];
final createEventContent = switch (createEvent?.content) {
CreateContent content => content,
_ => null,
};
final creators = createEventContent?.additionalCreatorIds.add(
createEvent!.sender,
);
final powerLevelsRowId = room?.state[EventType.powerLevels.type]?[""];
final powerLevelsEvent = powerLevelsRowId == null
? null
: room?.events[powerLevelsRowId];
final content = switch (powerLevelsEvent?.content) {
PowerLevelsContent content => content,
_ => PowerLevelsContent(),
};
final members = await ref.watch(
MembersByStatusController.provider(config).future,
);
return members.fold<IMap<int?, ISet<Event>>>(.new(), (result, event) {
final groupKey = creators?.contains(event.stateKey!) == true
? null
: content.users[event.stateKey!] ?? content.usersDefault;
return result.update(
groupKey,
(value) => value.add(event),
ifAbsent: () => .new({event}),
);
});
}
static final provider =
AsyncNotifierProvider.family<
MembersGroupedController,
IMap<int?, ISet<Event>>,
MembersByStatusConfig
>(MembersGroupedController.new);
}

View file

@ -14,17 +14,18 @@ import "package:nexus/models/relation_type.dart";
import "package:nexus/models/requests/send_message_request.dart";
import "package:nexus/models/room.dart";
class RoomChatController extends AsyncNotifier<IList<Event>> {
class RoomChatController extends AsyncNotifier<IList<Event>?> {
final String roomId;
RoomChatController(this.roomId);
@override
Future<IList<Event>> build() async {
Future<IList<Event>?> build() async {
final client = ref.watch(ClientController.provider.notifier);
final room = ref.watch(
RoomsController.provider.select((rooms) => rooms[roomId]),
);
if (room == null) return .new();
if (room == null) return null;
if (!room.hasFetchedState) {
final state = await client.getRoomState(.new(roomId: roomId));
@ -215,7 +216,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
}
static final provider = AsyncNotifierProvider.family
.autoDispose<RoomChatController, IList<Event>, String>(
.autoDispose<RoomChatController, IList<Event>?, String>(
RoomChatController.new,
);
}

View file

@ -88,6 +88,7 @@ class SpacesController extends Notifier<IList<Space>> {
final children = childRoomsBySpaceId[id] ?? .new();
return Space(
subSpaces: const IList.empty(), // TODO
id: id,
title: room.metadata?.name ?? "Unnamed Room",
room: room,
@ -103,12 +104,14 @@ class SpacesController extends Notifier<IList<Space>> {
title: "Home",
icon: Icons.home,
children: homeRooms,
subSpaces: .new(),
),
.new(
id: "dms",
title: "Direct Messages",
icon: Icons.people,
children: dmRooms,
subSpaces: .new(),
),
...topLevelSpacesList,
]

View file

@ -0,0 +1,2 @@
String? requiredValidator(String? value) =>
value == null || value.isEmpty ? "This field is required" : null;

View file

@ -1,4 +1,5 @@
import "dart:io";
import "package:dynamic_color/dynamic_color.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/foundation.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
@ -11,13 +12,12 @@ import "package:nexus/controllers/shared_prefs_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/pages/chat_page.dart";
import "package:nexus/pages/login_page.dart";
import "package:nexus/pages/select_server_page.dart";
import "package:nexus/pages/verify_page.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart";
import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart";
import "package:dynamic_system_colors/dynamic_system_colors.dart";
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@ -124,7 +124,7 @@ class App extends StatelessWidget {
}
if (!clientState.isLoggedIn) {
return LoginPage();
return SelectServerPage();
} else if (!clientState.isVerified) {
return VerifyPage();
} else {

View file

@ -8,8 +8,6 @@ part "create.g.dart";
abstract class CreateContent extends Content with _$CreateContent {
CreateContent._();
factory CreateContent({
@JsonKey(name: "creator") String? creatorId,
@JsonKey(name: "additional_creators")
@Default(IList.empty())
IList<String> additionalCreatorIds,

View file

@ -2,6 +2,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/widgets.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/room.dart";
import "package:nexus/models/subspace.dart";
part "space.freezed.dart";
@freezed
@ -12,5 +13,6 @@ abstract class Space with _$Space {
IconData? icon,
Room? room,
required IList<Room> children,
required IList<Subspace> subSpaces,
}) = _Space;
}

10
lib/models/subspace.dart Normal file
View file

@ -0,0 +1,10 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/room.dart";
part "subspace.freezed.dart";
@freezed
abstract class Subspace with _$Subspace {
const factory Subspace({required Room room, required IList<Room> children}) =
_Subspace;
}

View file

@ -1,205 +1,95 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/models/homeserver.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart";
import "package:nexus/widgets/loading.dart";
import "package:nexus/helpers/required_validator_helper.dart";
class LoginPage extends HookConsumerWidget {
const LoginPage({super.key});
final Uri homeserver;
const LoginPage({super.key, required this.homeserver});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final client = ref.watch(ClientController.provider.notifier);
final isLoading = useState(false);
final homeserver = useState<String?>(null);
final launch = ref.watch(LaunchHelper.provider).launchUrl;
Future<void> setHomeserver(Uri? newHomeserver) async {
isLoading.value = true;
homeserver.value = newHomeserver == null
? null
: await client.discoverHomeserver(
newHomeserver.hasScheme
? newHomeserver
: Uri.https(newHomeserver.path),
);
if (homeserver.value == null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
.new(
content: Text(
"Homeserver verification failed. Is your homeserver down?",
style: TextStyle(color: theme.colorScheme.onErrorContainer),
),
backgroundColor: theme.colorScheme.errorContainer,
),
);
}
isLoading.value = false;
}
final homeserverUrl = useTextEditingController();
final username = useTextEditingController();
final password = useTextEditingController();
final inputError = useState<String?>(null);
final formKey = useRef(GlobalKey<FormState>());
Future<void> tryLogin() async {
isLoading.value = true;
try {
if (formKey.value.currentState?.validate() != true) return;
final error = await client.login(
.new(
username: username.text,
password: password.text,
homeserverUrl: homeserver.origin,
),
);
if (error != null) {
inputError.value = error;
isLoading.value = false;
} else {
if (context.mounted) Navigator.of(context).pop();
}
} finally {
isLoading.value = false;
}
}
return Scaffold(
appBar: Appbar(),
body: Center(
child: ConstrainedBox(
constraints: .new(maxWidth: 600),
child: ListView(
padding: .symmetric(horizontal: 16, vertical: 64),
appBar: Appbar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: Navigator.of(context).pop,
),
),
body: AlertDialog(
title: Text("Login to ${homeserver.host}"),
content: Form(
key: formKey.value,
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Row(
children: [
SvgPicture.asset("assets/icon.svg", width: 128),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text("Nexus", style: theme.textTheme.displayMedium),
Text(
"A Simple Matrix Client",
style: theme.textTheme.headlineMedium,
overflow: .ellipsis,
),
],
),
),
],
TextFormField(
autofocus: true,
textInputAction: .next,
autovalidateMode: .onUserInteraction,
validator: requiredValidator,
decoration: .new(label: Text("Username")),
controller: username,
),
Padding(padding: .symmetric(vertical: 12), child: Divider()),
DividerText("Enter a homeserver domain:"),
Row(
spacing: 8,
children: [
Expanded(
child: TextField(
controller: homeserverUrl,
decoration: .new(
labelText: "Homeserver URL (e.g. matrix.org)",
),
),
),
IconButton.filled(
tooltip: "Confirm homeserver choice",
onPressed: isLoading.value
? null
: () => setHomeserver(.tryParse(homeserverUrl.text)),
icon: Icon(Icons.check),
),
],
SizedBox(height: 12),
TextFormField(
textInputAction: .done,
decoration: .new(
label: Text("Password"),
errorText: inputError.value,
errorMaxLines: 5,
),
autovalidateMode: .onUserInteraction,
validator: requiredValidator,
controller: password,
obscureText: true,
),
DividerText("Or, choose from some popular homeservers:"),
...(<Homeserver>[
.new(
name: "Matrix.org",
description:
"The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.",
url: .https("matrix.org"),
iconUrl:
"https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png",
),
.new(
name: "Federated Nexus",
description:
"Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.",
url: .https("federated.nexus"),
iconUrl: "https://federated.nexus/images/icon.png",
),
.new(
name: "Unredacted",
description:
"Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.",
url: .https("unredacted.org", "services/si/matrix"),
iconUrl: "https://unredacted.org/favicon.ico",
),
].map(
(homeserver) => Card(
child: ListTile(
title: Text(homeserver.name),
leading: Image.network(
homeserver.iconUrl,
errorBuilder: (_, _, _) => SizedBox.shrink(),
height: 32,
),
subtitle: Text(homeserver.description),
onTap: isLoading.value
? null
: () => setHomeserver(homeserver.url),
trailing: IconButton(
tooltip: "Launch homeserver info page",
onPressed: () => launch(homeserver.url),
icon: Icon(Icons.info_outline),
),
),
),
)),
SizedBox(height: 8),
TextButton(
onPressed: () => launch(.https("servers.joinmatrix.org")),
child: Text("See more homeservers..."),
),
if (isLoading.value)
Padding(padding: .only(top: 32), child: Loading())
else if (homeserver.value != null) ...[
DividerText("Then, sign in:"),
SizedBox(height: 4),
TextField(
decoration: .new(label: Text("Username")),
controller: username,
),
SizedBox(height: 12),
TextField(
decoration: .new(label: Text("Password")),
controller: password,
obscureText: true,
),
SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
isLoading.value = true;
final error = await client.login(
.new(
username: username.text,
password: password.text,
homeserverUrl: homeserver.value!,
),
);
if (error != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
.new(
content: Text(
"Login failed. Is your password right?\nError: $error",
style: .new(
color: theme.colorScheme.onErrorContainer,
),
),
backgroundColor: theme.colorScheme.errorContainer,
),
);
isLoading.value = false;
}
},
child: Text("Sign In"),
),
],
],
),
),
actions: [
TextButton(
onPressed: isLoading.value ? null : tryLogin,
child: Text("Sign In"),
),
],
),
);
}

View file

@ -0,0 +1,169 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/models/homeserver.dart";
import "package:nexus/pages/login_page.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart";
class SelectServerPage extends HookConsumerWidget {
const SelectServerPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final launch = ref.watch(LaunchHelper.provider).launchUrl;
final isLoading = useState(false);
final homeserverUrl = useTextEditingController();
Future<void> setHomeserver(Uri? newHomeserver) async {
isLoading.value = true;
try {
if (newHomeserver?.hasScheme == false) {
newHomeserver = Uri.https(newHomeserver!.path);
}
final newUrl = newHomeserver == null
? null
: await ref
.watch(ClientController.provider.notifier)
.discoverHomeserver(newHomeserver);
if (context.mounted) {
if (newUrl == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Homeserver verification failed. Is your homeserver down?",
style: .new(color: theme.colorScheme.onErrorContainer),
),
backgroundColor: theme.colorScheme.errorContainer,
),
);
} else {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)),
);
}
}
} finally {
isLoading.value = false;
}
}
return Scaffold(
appBar: Appbar(),
body: Center(
child: ConstrainedBox(
constraints: .new(maxWidth: 600),
child: ListView(
children: [
Row(
children: [
SvgPicture.asset("assets/icon.svg", width: 128),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text("Nexus", style: theme.textTheme.displayMedium),
Text(
"A Simple Matrix Client",
style: theme.textTheme.headlineMedium,
overflow: .ellipsis,
),
],
),
),
],
),
Padding(padding: .symmetric(vertical: 12), child: Divider()),
DividerText("Enter a homeserver domain:"),
Row(
spacing: 8,
children: [
Expanded(
child: TextField(
textInputAction: .done,
autofocus: true,
onSubmitted: (text) => setHomeserver(.tryParse(text)),
controller: homeserverUrl,
decoration: .new(
labelText: "Homeserver URL",
hintText: "matrix.org",
),
),
),
IconButton.filled(
tooltip: "Confirm homeserver choice",
onPressed: isLoading.value
? null
: () => setHomeserver(.tryParse(homeserverUrl.text)),
icon: Icon(Icons.check),
),
],
),
DividerText("Or, choose from some popular homeservers:"),
...(<Homeserver>[
.new(
name: "Matrix.org",
description:
"The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.",
url: .https("matrix.org"),
iconUrl:
"https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png",
),
.new(
name: "Federated Nexus",
description:
"Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.",
url: .https("federated.nexus"),
iconUrl: "https://federated.nexus/images/icon.png",
),
.new(
name: "Unredacted",
description:
"Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.",
url: .https("unredacted.org", "services/si/matrix"),
iconUrl: "https://unredacted.org/favicon.ico",
),
].map(
(homeserver) => Card(
child: ListTile(
enabled: !isLoading.value,
title: Text(homeserver.name),
leading: Image.network(
homeserver.iconUrl,
errorBuilder: (_, _, _) => SizedBox.shrink(),
height: 32,
),
subtitle: Text(homeserver.description),
onTap: isLoading.value
? null
: () => setHomeserver(homeserver.url),
trailing: IconButton(
tooltip: "Launch homeserver info page",
onPressed: () => launch(homeserver.url),
icon: Icon(Icons.info_outline),
),
),
),
)),
TextButton(
onPressed: () => launch(.https("servers.joinmatrix.org")),
child: Text("See more homeservers..."),
),
],
),
),
),
);
}
}

View file

@ -3,7 +3,7 @@ import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/form_text_input.dart";
import "package:nexus/helpers/required_validator_helper.dart";
class VerifyPage extends HookConsumerWidget {
const VerifyPage({super.key});
@ -11,70 +11,56 @@ class VerifyPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final passphraseController = useTextEditingController();
final isVerifying = useState(false);
final isLoading = useState(false);
final inputError = useState<String?>(null);
final formKey = useRef(GlobalKey<FormState>());
return Scaffold(
appBar: Appbar(),
body: AlertDialog(
title: Text("Verify"),
content: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
),
SizedBox(height: 12),
FormTextInput(
required: false,
autofocus: true,
capitalize: true,
controller: passphraseController,
obscure: true,
title: "Recovery Key or Passphrase",
),
],
content: Form(
key: formKey.value,
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
),
SizedBox(height: 12),
TextFormField(
autofocus: true,
controller: passphraseController,
textInputAction: .done,
autovalidateMode: .onUserInteraction,
validator: requiredValidator,
obscureText: true,
decoration: .new(
label: Text("Recovery Key or Passphrase"),
errorText: inputError.value,
),
),
],
),
),
actions: [
TextButton(
onPressed: isVerifying.value
onPressed: isLoading.value
? null
: () async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final snackbar = scaffoldMessenger.showSnackBar(
.new(
content: Text(
"Attempting to verify with recovery key...",
),
duration: .new(days: 999),
),
);
isLoading.value = true;
isVerifying.value = true;
final error = await ref
.watch(ClientController.provider.notifier)
.verify(passphraseController.text);
snackbar.close();
if (error != null) {
isVerifying.value = false;
if (context.mounted) {
scaffoldMessenger.showSnackBar(
.new(
backgroundColor: Theme.of(
context,
).colorScheme.errorContainer,
content: Text(
"Verification failed. Is your passphrase correct?\nError: $error",
style: .new(
color: Theme.of(
context,
).colorScheme.onErrorContainer,
),
),
),
);
try {
if (formKey.value.currentState?.validate() != true) {
return;
}
inputError.value = await ref
.watch(ClientController.provider.notifier)
.verify(passphraseController.text);
} finally {
isLoading.value = false;
}
},
child: Text("Verify"),

View file

@ -11,15 +11,11 @@ class AvatarOrHash extends ConsumerWidget {
final Uri? avatar;
final String title;
final Widget? fallback;
final bool hasBadge;
final int badgeNumber;
final double height;
const AvatarOrHash(
this.avatar,
this.title, {
this.fallback,
this.badgeNumber = 0,
this.hasBadge = false,
this.height = 24,
super.key,
});
@ -44,30 +40,24 @@ class AvatarOrHash extends ConsumerWidget {
width: height,
height: height,
child: Center(
child: Badge(
isLabelVisible: hasBadge,
label: badgeNumber != 0 ? Text(badgeNumber.toString()) : null,
smallSize: 12,
backgroundColor: Theme.of(context).colorScheme.primary,
child: ClipRRect(
borderRadius: .all(.circular((height - 8) / 2.5)),
child: SizedBox(
width: height,
height: height,
child: parsedAvatar == null
? fallback ?? box
: Image(
image: CachedNetworkImage(
parsedAvatar.toString(),
ref.watch(CrossCacheController.provider),
headers: ref.headers,
),
fit: .cover,
loadingBuilder: (_, child, loadingProgress) =>
loadingProgress == null ? child : fallback ?? box,
errorBuilder: (_, _, _) => fallback ?? box,
child: ClipRRect(
borderRadius: .all(.circular((height - 8) / 2.5)),
child: SizedBox(
width: height,
height: height,
child: parsedAvatar == null
? fallback ?? box
: Image(
image: CachedNetworkImage(
parsedAvatar.toString(),
ref.watch(CrossCacheController.provider),
headers: ref.headers,
),
),
fit: .cover,
loadingBuilder: (_, child, loadingProgress) =>
loadingProgress == null ? child : fallback ?? box,
errorBuilder: (_, _, _) => fallback ?? box,
),
),
),
),

View file

@ -1,5 +1,6 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:fluttertagger/fluttertagger.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
@ -135,21 +136,34 @@ class Composer extends HookConsumerWidget {
query.value = newQuery;
},
triggerCharacterAndStyles: {"@": style, "#": style},
builder: (context, key) => TextField(
maxLines: 12,
minLines: 1,
autofocus: true,
decoration: .new(
hintText: "Your message here...",
border: .none,
builder: (context, key) => Focus(
onKeyEvent: (_, event) {
if (event is KeyDownEvent &&
event.logicalKey ==
LogicalKeyboardKey.enter) {
final shiftPressed =
HardwareKeyboard.instance.isShiftPressed;
if (!shiftPressed) {
send();
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
child: TextField(
maxLines: 12,
minLines: 1,
autofocus: true,
decoration: .new(
hintText: "Your message here...",
border: .none,
),
controller: controller.value,
key: key,
focusNode: node,
),
controller: controller.value,
key: key,
onSubmitted: (_) => send(),
// Don't defocus on submit
onEditingComplete: () {},
textInputAction: .done,
focusNode: node,
),
),
),

View file

@ -1,81 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class FormTextInput extends StatelessWidget {
final List<String? Function(String value)> extraValidators;
final TextEditingController? controller;
final TextInputType keyboardType;
final String? initialValue;
final bool readOnly;
final bool obscure;
final String? title;
final int? minLines;
final int? maxLength;
final bool outlined;
final int? maxLines;
final bool capitalize;
final bool required;
final bool autocorrect;
final void Function()? onTap;
final Widget? trailing;
final InputBorder? border;
final List<TextInputFormatter>? formatters;
final bool autofocus;
const FormTextInput({
super.key,
this.border,
this.controller,
this.autofocus = false,
this.title,
this.obscure = false,
this.readOnly = false,
this.extraValidators = const [],
this.keyboardType = TextInputType.text,
this.initialValue,
this.minLines,
this.capitalize = false,
this.maxLength,
this.formatters,
this.maxLines = 1,
this.outlined = true,
this.trailing,
this.onTap,
this.autocorrect = true,
this.required = true,
});
@override
Widget build(BuildContext context) => TextFormField(
autofocus: autofocus,
controller: controller,
keyboardType: keyboardType,
readOnly: readOnly,
minLines: minLines,
maxLines: maxLines,
maxLength: maxLength,
inputFormatters: formatters,
textCapitalization: capitalize ? .sentences : .none,
initialValue: initialValue,
autocorrect: autocorrect,
obscureText: obscure,
onTap: onTap,
decoration: .new(
labelText: title,
border: border ?? (outlined ? null : const UnderlineInputBorder()),
suffixIcon: trailing,
),
validator: (value) {
if ((value?.isEmpty ?? true) && required) {
return "This field is required";
}
for (final validator in extraValidators) {
final reason = validator(value!);
if (reason != null) return reason;
}
return null;
},
);
}

View file

@ -6,7 +6,6 @@ import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/helpers/extensions/link_to_mention.dart";
import "package:nexus/widgets/form_text_input.dart";
class JoinDialog extends HookWidget {
final WidgetRef ref;
@ -23,11 +22,9 @@ class JoinDialog extends HookWidget {
children: [
Text("Enter the room alias, Matrix URI, or Matrix.to link."),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
TextField(
controller: roomAlias,
title: "#room:server",
decoration: .new(hintText: "#room:server"),
),
],
),
@ -87,9 +84,15 @@ class JoinDialog extends HookWidget {
space?.id ??
spaces
.firstWhere(
(space) => space.children.any(
(child) => child.metadata?.id == id,
),
(space) =>
space.children.any(
(child) =>
child.metadata?.id == id,
) ||
space.subSpaces.any(
(child) =>
child.room.metadata?.id == id,
),
)
.id,
);

View file

@ -1,13 +1,16 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/members_by_status_controller.dart";
import "package:m3e_buttons/m3e_buttons.dart";
import "package:material_segmented_list/material_segmented_list.dart";
import "package:nexus/controllers/members_grouped_controller.dart";
import "package:nexus/helpers/extensions/get_localpart.dart";
import "package:nexus/helpers/extensions/show_user_popover.dart";
import "package:nexus/helpers/extensions/string_to_color.dart";
import "package:nexus/models/content/membership.dart";
import "package:nexus/models/membership_status.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/divider_text.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart";
@ -18,12 +21,19 @@ class MemberList extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = useState(MembershipStatus.join);
final membersProvider = ref.watch(
MembersByStatusController.provider(
final membersData = ref.watch(
MembersGroupedController.provider(
.new(roomId: roomId, status: status.value),
),
);
final options = <String, MembershipStatus>{
"Joined": .join,
"Invited": .invite,
"Banned": .ban,
};
return Drawer(
shape: Border(),
child: Column(
@ -42,74 +52,107 @@ class MemberList extends HookConsumerWidget {
),
],
),
Wrap(
alignment: .center,
spacing: 8,
runSpacing: 8,
children: [
FilterChip(
label: Text("Joined"),
onSelected: (value) => status.value = .join,
selected: status.value == .join,
),
FilterChip(
label: Text("Invited"),
onSelected: (value) => status.value = .invite,
selected: status.value == .invite,
),
FilterChip(
label: Text("Banned"),
onSelected: (value) => status.value = .ban,
selected: status.value == .ban,
),
],
M3EToggleButtonGroup(
type: .connected,
selectedIndex: options.values.toIList().indexOf(status.value),
onSelectedIndexChanged: (index) =>
status.value = options.values.elementAt(index ?? 0),
// overflow: M3EButtonGroupOverflow.menu,
actions: options
.mapTo(
(name, value) => M3EToggleButtonGroupAction(
checkedLabel: Text(
"$name${switch (membersData) {
AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.values.expand((element) => element).length})",
_ => "",
}}",
),
label: Text(name),
),
)
.toList(),
),
switch (membersProvider) {
switch (membersData) {
AsyncError(:final error, :final stackTrace) => ErrorDialog(
error,
stackTrace,
),
AsyncData(:final value) || AsyncLoading(:final value?) => Expanded(
child: ListView(
children: value
.map(
(member) => switch (member.content) {
MembershipContent(
:final avatarUrl,
:final displayName,
) =>
InkWell(
onTapUp: (details) => context.showUserPopover(
member.content as MembershipContent,
member.stateKey!,
roomId: roomId,
globalPosition: details.globalPosition,
),
child: ListTile(
leading: AvatarOrHash(
avatarUrl,
displayName ?? member.sender.localpart,
),
title: Text(
displayName ?? member.stateKey!.localpart,
overflow: .ellipsis,
style: .new(
color: member.stateKey!.colorHash,
fontWeight: .bold,
),
),
subtitle: Text(
member.stateKey!,
overflow: .ellipsis,
),
),
),
_ => SizedBox.shrink(),
},
AsyncData(:final value) || AsyncLoading(:final value?) =>
value.isEmpty
? Center(
child: Padding(
padding: .symmetric(vertical: 18),
child: Text(
"No ${options.keys.toIList()[options.values.toIList().indexOf(status.value)]} Members",
style: Theme.of(context).textTheme.headlineSmall,
),
),
)
.toList(),
),
),
: Expanded(
child: ListView(
padding: .all(12),
children: [
for (final MapEntry(key: powerLevel, value: members)
in value.toEntryIList(
compare: (a, b) => (b?.key ?? double.infinity)
.compareTo(a?.key ?? double.infinity),
)) ...[
Padding(
padding: .symmetric(horizontal: 4),
child: DividerText(
powerLevel == null
? "Creators"
: "Power Level $powerLevel",
),
),
SegmentedListSection(
children: members
.map(
(member) => switch (member.content) {
MembershipContent(
:final avatarUrl,
:final displayName,
) =>
SegmentedListTile(
onTap: () {},
// context.showUserPopover(
// member.content as MembershipContent,
// member.stateKey!,
// roomId: roomId,
// globalPosition: details.globalPosition,
// ),
title: Text(
displayName ??
member.stateKey!.localpart,
overflow: .ellipsis,
style: .new(
color: member.stateKey!.colorHash,
fontWeight: .bold,
),
),
subtitle: Text(
member.stateKey!,
overflow: .ellipsis,
),
leading: AvatarOrHash(
avatarUrl,
displayName ??
member.sender.localpart,
),
),
_ => throw Exception(
"Member content was not MembershipContent",
),
},
)
.toList(),
),
SizedBox(height: 4),
],
],
),
),
AsyncLoading _ => Loading(),
},
],

View file

@ -1,4 +1,3 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/rooms_controller.dart";
@ -93,13 +92,15 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
),
),
),
leading: isDesktop && room != null
? AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
height: 24,
fallback: Icon(Icons.numbers),
)
leading: isDesktop
? room == null
? null
: AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
height: 24,
fallback: Icon(Icons.numbers),
)
: DrawerButton(onPressed: () => onOpenDrawer(context)),
scrolledUnderElevation: 0,
title: room == null
@ -123,19 +124,21 @@ class RoomAppbar extends ConsumerWidget implements PreferredSizeWidget {
),
],
),
actions: [
IconButton(
onPressed: null,
icon: Icon(Icons.push_pin),
tooltip: "Open pinned messages",
),
IconButton(
onPressed: () => onOpenMemberList?.call(context),
tooltip: "Open member list",
icon: Icon(Icons.people),
),
if (room != null) RoomMenu(room),
].toIList(),
actions: room == null
? .new()
: .new([
IconButton(
onPressed: null,
icon: Icon(Icons.push_pin),
tooltip: "Open pinned messages",
),
IconButton(
onPressed: () => onOpenMemberList?.call(context),
tooltip: "Open member list",
icon: Icon(Icons.people),
),
RoomMenu(room),
]),
);
}
}

View file

@ -22,7 +22,6 @@ import "package:nexus/widgets/member_list.dart";
import "package:nexus/widgets/room_appbar.dart";
import "package:nexus/widgets/flash_wrapper.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/form_text_input.dart";
import "package:nexus/main.dart";
import "package:nexus/widgets/loading.dart";
import "package:super_sliver_list/super_sliver_list.dart";
@ -51,6 +50,12 @@ class RoomChat extends HookConsumerWidget {
final userId = ref.watch(ClientStateController.provider)?.userId;
final theme = Theme.of(context);
final nothing = Center(
child: Text(
"Nothing to see here...",
style: theme.textTheme.headlineMedium,
),
);
if (userId == null || this.roomId == null) {
return Scaffold(
appBar: RoomAppbar(
@ -59,12 +64,7 @@ class RoomChat extends HookConsumerWidget {
onOpenDrawer: (_) => Scaffold.of(context).openDrawer(),
onOpenMemberList: null,
),
body: Center(
child: Text(
"Nothing to see here...",
style: theme.textTheme.headlineMedium,
),
),
body: nothing,
);
}
@ -82,7 +82,7 @@ class RoomChat extends HookConsumerWidget {
final topEventBeforeLoad = useState<String?>(null);
Future<void> loadOlder() async {
if (controllerData case AsyncData(:final value)) {
if (controllerData case AsyncData(:final value?)) {
topEventBeforeLoad.value = value.firstOrNull?.eventId;
await notifier.loadOlder();
}
@ -106,7 +106,7 @@ class RoomChat extends HookConsumerWidget {
useEffect(() {
if (controllerData case AsyncData(
:final value,
:final value?,
) when scrollController.hasClients) {
if (topEventBeforeLoad.value != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -288,11 +288,12 @@ class RoomChat extends HookConsumerWidget {
"Are you sure you want to delete this message? This can not be reversed.",
),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
TextField(
controller: deleteReasonController,
title: "Reason for deletion (optional)",
textCapitalization: .sentences,
decoration: .new(
labelText: "Reason for deletion (optional)",
),
),
],
),
@ -340,11 +341,12 @@ class RoomChat extends HookConsumerWidget {
),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
TextField(
controller: reasonController,
title: "Reason for report (optional)",
textCapitalization: .sentences,
decoration: .new(
labelText: "Reason for report (optional)",
),
),
],
),
@ -400,7 +402,7 @@ class RoomChat extends HookConsumerWidget {
child: Padding(
padding: .symmetric(horizontal: 12),
child: switch (controllerData) {
AsyncData(:final value) ||
AsyncData(:final value?) ||
AsyncLoading(:final value?) => CustomScrollView(
controller: scrollController,
slivers: [
@ -466,6 +468,7 @@ class RoomChat extends HookConsumerWidget {
),
],
),
AsyncData() => nothing,
AsyncLoading() => Loading(),
AsyncError(:final error, :final stackTrace) =>
ErrorDialog(error, stackTrace),

View file

@ -1,8 +1,11 @@
import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
import "package:nexus/controllers/key_controller.dart";
import "package:nexus/controllers/spaces_controller.dart";
import "package:nexus/models/room.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/join_dialog.dart";
import "package:nexus/widgets/room_menu.dart";
@ -39,88 +42,136 @@ class Sidebar extends HookConsumerWidget {
(room) => room.metadata?.id == selectedRoomId,
);
final selectedRoomIndex = indexOfSelectedRoom == -1
? selectedSpace.children.isEmpty
? null
: 0
? null
: indexOfSelectedRoom;
List<NavigationRailM3EDestination> roomsToDestinations(IList<Room> rooms) =>
rooms
.map(
(room) => NavigationRailM3EDestination(
label: room.metadata?.name ?? "Unnamed Room",
badgeCount: switch (room.metadata?.unreadNotifications) {
0 || null => room.metadata?.unreadMessages == 0 ? null : 0,
int unread => unread,
},
icon: AvatarOrHash(
room.metadata?.avatar,
room.metadata?.name ?? "Unnamed Room",
fallback: selectedSpaceId == "dms"
? null
: Icon(Icons.numbers),
// space.client.headers,
),
),
)
.toList();
return Drawer(
width: 340,
shape: Border(),
child: Row(
children: [
NavigationRail(
scrollable: true,
onDestinationSelected: (value) {
selectedSpaceIdNotifier.set(spaces[value].id);
selectedRoomIdNotifier.set(
spaces[value].children.firstOrNull?.metadata?.id,
);
},
destinations: spaces
.map(
(space) => NavigationRailDestination(
icon: AvatarOrHash(
space.room?.metadata?.avatar,
fallback: space.icon == null ? null : Icon(space.icon),
space.title,
hasBadge: space.children.any(
(room) => room.metadata?.unreadMessages != 0,
),
badgeNumber: space.children.fold(
0,
(previousValue, room) =>
previousValue +
(room.metadata?.unreadNotifications ?? 0),
),
),
label: Text(space.title),
padding: .only(top: 4),
),
)
.toList(),
selectedIndex: selectedIndex,
trailingAtBottom: true,
trailing: Padding(
padding: .symmetric(vertical: 16),
child: Column(
spacing: 8,
children: [
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (_) => JoinDialog(ref),
),
child: ListTile(
title: Text("Join an existing room (or space)"),
leading: Icon(Icons.numbers),
),
),
PopupMenuItem(
onTap: null,
child: ListTile(
title: Text("Create a new room"),
leading: Icon(Icons.add),
),
),
],
icon: Icon(Icons.add),
),
IconButton(
tooltip: "Explore other rooms",
onPressed: null,
icon: Icon(Icons.explore),
),
IconButton(
tooltip: "Open settings",
onPressed: null,
// () => Navigator.of(
// context,
// ).push(MaterialPageRoute(builder: (_) => SettingsPage())),
icon: Icon(Icons.settings),
Theme(
data: Theme.of(context).copyWith(
extensions: [
NavigationRailM3ETheme(
itemCollapsedHeight: 48,
itemVerticalGap: 0,
),
],
),
child: Container(
color: NavigationRailTokensAdapter(context).containerColor,
padding: EdgeInsets.only(top: 16),
child: NavigationRailM3E(
type: .alwaysCollapse,
labelBehavior: .alwaysHide,
scrollable: true,
onDestinationSelected: (value) {
selectedSpaceIdNotifier.set(spaces[value].id);
selectedRoomIdNotifier.set(
spaces[value].children.firstOrNull?.metadata?.id,
);
},
sections: [
.new(
destinations: spaces
.map(
(space) => NavigationRailM3EDestination(
badgeCount: switch (space.children.fold(
0,
(previousValue, room) =>
previousValue +
(room.metadata?.unreadNotifications ?? 0),
)) {
0 =>
space.children.any(
(room) =>
room.metadata?.unreadMessages != 0,
)
? 0
: null,
int badgeCount => badgeCount,
},
short: true,
icon: AvatarOrHash(
space.room?.metadata?.avatar,
fallback: space.icon == null
? null
: Icon(space.icon),
space.title,
),
label: space.title,
),
)
.toList(),
),
],
selectedIndex: selectedIndex,
trailingAtBottom: true,
trailing: Padding(
padding: .symmetric(vertical: 16),
child: Column(
spacing: 8,
children: [
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
onTap: () => showDialog(
context: context,
builder: (_) => JoinDialog(ref),
),
child: ListTile(
title: Text("Join an existing room (or space)"),
leading: Icon(Icons.numbers),
),
),
PopupMenuItem(
onTap: null,
child: ListTile(
title: Text("Create a new room"),
leading: Icon(Icons.add),
),
),
],
icon: Icon(Icons.add),
),
IconButton(
tooltip: "Explore other rooms",
onPressed: null,
icon: Icon(Icons.explore),
),
IconButton(
tooltip: "Open settings",
onPressed: null,
// () => Navigator.of(
// context,
// ).push(MaterialPageRoute(builder: (_) => SettingsPage())),
icon: Icon(Icons.settings),
),
],
),
),
),
),
),
@ -145,34 +196,40 @@ class Sidebar extends HookConsumerWidget {
),
],
),
body: NavigationRail(
scrollable: true,
backgroundColor: Colors.transparent,
extended: true,
selectedIndex: selectedRoomIndex,
destinations: selectedSpace.children
.map(
(room) => NavigationRailDestination(
label: Text(room.metadata?.name ?? "Unnamed Room"),
icon: AvatarOrHash(
room.metadata?.avatar,
hasBadge: room.metadata?.unreadMessages != 0,
badgeNumber: room.metadata?.unreadNotifications ?? 0,
room.metadata?.name ?? "Unnamed Room",
fallback: selectedSpaceId == "dms"
? null
: Icon(Icons.numbers),
// space.client.headers,
body: Theme(
data: Theme.of(context).copyWith(
extensions: [
NavigationRailM3ETheme(
itemExpandedHeight: 48,
iconLabelGap: 16,
),
],
),
child: NavigationRailM3E(
expandedWidth: 360,
scrollable: true,
background: Colors.transparent,
type: .alwaysExpand,
selectedIndex: selectedRoomIndex ?? 0,
sections: [
.new(
destinations: roomsToDestinations(selectedSpace.children),
),
for (final subSpace in selectedSpace.subSpaces)
.new(
header: Text(
subSpace.room.metadata?.name ?? "Unnamed Room",
),
destinations: roomsToDestinations(subSpace.children),
),
)
.toList(),
onDestinationSelected: (value) {
selectedRoomIdNotifier.set(
selectedSpace.children[value].metadata?.id,
);
if (!isDesktop) Navigator.of(context).pop();
},
],
onDestinationSelected: (value) {
selectedRoomIdNotifier.set(
selectedSpace.children[value].metadata?.id,
);
if (!isDesktop) Navigator.of(context).pop();
},
),
),
),
),

View file

@ -14,7 +14,6 @@ import "package:nexus/models/requests/membership_action.dart";
import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/main.dart";
import "package:nexus/widgets/expandable_image.dart";
import "package:nexus/widgets/form_text_input.dart";
class UserPopover extends ConsumerWidget {
final MembershipContent member;
@ -41,11 +40,12 @@ class UserPopover extends ConsumerWidget {
children: [
Text("Are you sure you want to ${action.name} $userId?"),
SizedBox(height: 12),
FormTextInput(
required: false,
capitalize: true,
TextField(
textCapitalization: .sentences,
controller: actionReasonController,
title: "Reason for ${action.name} (optional)",
decoration: .new(
labelText: "Reason for ${action.name} (optional)",
),
),
],
),

View file

@ -6,7 +6,7 @@
#include "generated_plugin_registrant.h"
#include <dynamic_system_colors/dynamic_color_plugin.h>
#include <dynamic_color/dynamic_color_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
@ -15,9 +15,9 @@
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_system_colors_registrar =
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_system_colors_registrar);
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);

View file

@ -3,7 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
dynamic_color
file_selector_linux
media_kit_libs_linux
media_kit_video

View file

@ -30,9 +30,9 @@ flutter.buildFlutterApplication {
gitHashes = {
window_size = "sha256-XelNtp7tpZ91QCEcvewVphNUtgQX7xrp5QP0oFo6DgM=";
dynamic_system_colors = "sha256-GInPqU7r4Kj7+CNBQnf95u0BiagOUI6EtcW0A18pfd0=";
emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo=";
linkify = "sha256-mxV/XHLxF9cn7sUPr2SUNjVmDr5lbxkuGCbNdyiZi2c=";
navigation_rail_m3e = "sha256-Zkqo8RkNSWiELB+Bk6iLsgft9THuJiMtysEwXNS/ZgY=";
};
postInstall = ''

View file

@ -121,6 +121,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.12.6"
button_m3e:
dependency: transitive
description:
name: button_m3e
sha256: "6754ddeb9068ad2005bd26d5ceabc41268029465095686d7d228296c2e706909"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
characters:
dependency: transitive
description:
@ -273,6 +281,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c"
url: "https://pub.dev"
source: hosted
version: "1.8.1"
dynamic_polls:
dependency: "direct main"
description:
@ -281,15 +297,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.7"
dynamic_system_colors:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: fc7eea96556aebb1a7cd3272929277bc6d136233
url: "https://github.com/hasali19/flutter_dynamic_system_colors"
source: git
version: "1.9.0"
emoji_text_field:
dependency: "direct main"
description:
@ -299,6 +306,22 @@ packages:
url: "https://github.com/Henry-Hiles/emoji_text_field"
source: git
version: "1.0.0"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fab_m3e:
dependency: transitive
description:
name: fab_m3e
sha256: e4f5abfa3c8c092005449d56dcac45b85e2dbe9c32789d672c5ed71428e43b59
url: "https://pub.dev"
source: hosted
version: "0.1.1"
fake_async:
dependency: transitive
description:
@ -583,6 +606,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
icon_button_m3e:
dependency: transitive
description:
name: icon_button_m3e
sha256: c4524d6141a468679821bbb635b833ac6831925d8a6ae4a4511430b0e4ab9c67
url: "https://pub.dev"
source: hosted
version: "0.2.1"
idb_shim:
dependency: transitive
description:
@ -744,6 +775,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
m3e_buttons:
dependency: "direct main"
description:
name: m3e_buttons
sha256: "50cdf9ba30fb3ab529afafb0e837484549f8599f1f109ac07da50951febaace1"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
m3e_design:
dependency: transitive
description:
name: m3e_design
sha256: "15ff0ef4c43553d855c5e866a9aee8231d44919fe2bb354b1259337bdfd659b4"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
matcher:
dependency: transitive
description:
@ -760,6 +807,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.0"
material_segmented_list:
dependency: "direct main"
description:
name: material_segmented_list
sha256: "384bfd41a78e745397ceff1dd39700961e6a5419ad911d1797bcc13ea3824241"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
measure_size:
dependency: "direct main"
description:
@ -848,6 +903,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
motor:
dependency: transitive
description:
name: motor
sha256: cbd49f21b00e568c2b1a55f134ed803614a107782f4fea7769693bca32940c58
url: "https://pub.dev"
source: hosted
version: "1.1.0"
native_toolchain_c:
dependency: transitive
description:
@ -856,6 +919,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.17.6"
navigation_rail_m3e:
dependency: "direct main"
description:
path: "packages/navigation_rail_m3e"
ref: HEAD
resolved-ref: "667b0bc8526fd53296778903b6ef3f22424f3aa4"
url: "https://github.com/Henry-Hiles/material_3_expressive"
source: git
version: "0.3.5"
node_preamble:
dependency: transitive
description:

View file

@ -33,9 +33,7 @@ dependencies:
image_picker: 1.2.2
file_picker: 11.0.2
path: 1.9.1
dynamic_system_colors:
git:
url: https://github.com/hasali19/flutter_dynamic_system_colors
dynamic_color: 1.8.1
collection: 1.19.1
window_manager: 0.5.1
color_hash: 1.0.1
@ -64,6 +62,12 @@ dependencies:
media_kit_video: 2.0.1
media_kit_libs_video: 1.0.7
measure_size: ^5.0.2
material_segmented_list: ^1.0.5
m3e_buttons: ^0.0.3
navigation_rail_m3e:
git:
url: https://github.com/Henry-Hiles/material_3_expressive
path: packages/navigation_rail_m3e
dev_dependencies:
build_runner: 2.15.0

View file

@ -6,7 +6,7 @@
#include "generated_plugin_registrant.h"
#include <dynamic_system_colors/dynamic_color_plugin_c_api.h>
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.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>

View file

@ -3,7 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_system_colors
dynamic_color
file_selector_windows
media_kit_libs_windows_video
media_kit_video