add profile popovers
This commit is contained in:
parent
7ee165b300
commit
0b9ddbfbc8
11 changed files with 302 additions and 149 deletions
17
lib/controllers/profile_controller.dart
Normal file
17
lib/controllers/profile_controller.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client_controller.dart";
|
||||||
|
import "package:nexus/models/profile.dart";
|
||||||
|
|
||||||
|
class ProfileController extends AsyncNotifier<Profile> {
|
||||||
|
final String userId;
|
||||||
|
ProfileController(this.userId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Profile> build() {
|
||||||
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
return client.getProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.autoDispose
|
||||||
|
.family<ProfileController, Profile, String>(ProfileController.new);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,15 @@ extension SchemeToTheme on ColorScheme {
|
||||||
titleSpacing: 0,
|
titleSpacing: 0,
|
||||||
backgroundColor: surfaceContainerLow,
|
backgroundColor: surfaceContainerLow,
|
||||||
),
|
),
|
||||||
|
menuTheme: MenuThemeData(
|
||||||
|
style: MenuStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(primaryContainer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
chipTheme: ChipThemeData(
|
||||||
|
labelStyle: TextStyle(color: onPrimary),
|
||||||
|
color: WidgetStatePropertyAll(primary),
|
||||||
|
),
|
||||||
textTheme: ThemeData(
|
textTheme: ThemeData(
|
||||||
fontFamilyFallback: ["sans", "emoji"],
|
fontFamilyFallback: ["sans", "emoji"],
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ extension ShowContextMenu on BuildContext {
|
||||||
|
|
||||||
showMenu(
|
showMenu(
|
||||||
context: this,
|
context: this,
|
||||||
|
constraints: BoxConstraints.loose(Size.infinite),
|
||||||
position: RelativeRect.fromLTRB(
|
position: RelativeRect.fromLTRB(
|
||||||
globalPosition.dx,
|
globalPosition.dx,
|
||||||
globalPosition.dy,
|
globalPosition.dy,
|
||||||
|
|
|
||||||
18
lib/helpers/extensions/show_user_popover.dart
Normal file
18
lib/helpers/extensions/show_user_popover.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_context_menu.dart";
|
||||||
|
import "package:nexus/models/membership.dart";
|
||||||
|
import "package:nexus/widgets/chat_page/user_popover.dart";
|
||||||
|
|
||||||
|
extension ShowUserPopover on BuildContext {
|
||||||
|
void showUserPopover(Membership member, {required Offset globalPosition}) =>
|
||||||
|
showContextMenu(
|
||||||
|
globalPosition: globalPosition,
|
||||||
|
children: [
|
||||||
|
PopupMenuItem(
|
||||||
|
enabled: false,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: IconTheme(data: IconThemeData(), child: UserPopover(member)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ class AvatarOrHash extends ConsumerWidget {
|
||||||
ref.watch(CrossCacheController.provider),
|
ref.watch(CrossCacheController.provider),
|
||||||
headers: ref.headers,
|
headers: ref.headers,
|
||||||
),
|
),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, _, _) => box,
|
errorBuilder: (_, _, _) => box,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -21,133 +21,135 @@ class Html extends ConsumerWidget {
|
||||||
const Html(this.html, {this.textStyle, super.key});
|
const Html(this.html, {this.textStyle, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) => HtmlWidget(
|
Widget build(BuildContext context, WidgetRef ref) => SelectionArea(
|
||||||
html,
|
child: HtmlWidget(
|
||||||
textStyle: textStyle,
|
html,
|
||||||
customWidgetBuilder: (element) {
|
textStyle: textStyle,
|
||||||
if (element.attributes.keys.contains("data-mx-profile-fallback")) {
|
customWidgetBuilder: (element) {
|
||||||
return SizedBox.shrink();
|
if (element.attributes.keys.contains("data-mx-profile-fallback")) {
|
||||||
}
|
return SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
if (element.attributes.keys.contains("data-mx-spoiler")) {
|
if (element.attributes.keys.contains("data-mx-spoiler")) {
|
||||||
return InlineCustomWidget(child: SpoilerText(text: element.text));
|
return InlineCustomWidget(child: SpoilerText(text: element.text));
|
||||||
}
|
}
|
||||||
|
|
||||||
final height = int.tryParse(element.attributes["height"] ?? "") ?? 300;
|
final height = int.tryParse(element.attributes["height"] ?? "") ?? 300;
|
||||||
final width = int.tryParse(element.attributes["width"] ?? "");
|
final width = int.tryParse(element.attributes["width"] ?? "");
|
||||||
final src = Uri.tryParse(element.attributes["src"] ?? "")
|
final src = Uri.tryParse(element.attributes["src"] ?? "")
|
||||||
?.mxcToHttps(
|
?.mxcToHttps(
|
||||||
ref.watch(
|
ref.watch(
|
||||||
ClientStateController.provider.select(
|
ClientStateController.provider.select(
|
||||||
(value) => value?.homeserverUrl,
|
(value) => value?.homeserverUrl,
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
return switch (element.localName) {
|
return switch (element.localName) {
|
||||||
"code" =>
|
"code" =>
|
||||||
element.parent?.localName == "pre"
|
element.parent?.localName == "pre"
|
||||||
? element.outerHtml.contains("<br class=\"fake-break\">")
|
? element.outerHtml.contains("<br class=\"fake-break\">")
|
||||||
? Html(
|
? Html(
|
||||||
"""<pre>${element.outerHtml.replaceAll("<br class=\"fake-break\">", "\n")}</pre>""",
|
"""<pre>${element.outerHtml.replaceAll("<br class=\"fake-break\">", "\n")}</pre>""",
|
||||||
)
|
)
|
||||||
: CodeBlock(
|
: CodeBlock(
|
||||||
element.text,
|
element.text,
|
||||||
lang: element.className.replaceAll("language-", ""),
|
lang: element.className.replaceAll("language-", ""),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
"blockquote" => Quoted(Html(element.innerHtml)),
|
"blockquote" => Quoted(Html(element.innerHtml)),
|
||||||
|
|
||||||
"a" =>
|
"a" =>
|
||||||
element.attributes["href"]?.mention == null
|
element.attributes["href"]?.mention == null
|
||||||
? null
|
? null
|
||||||
: InlineCustomWidget(child: MentionChip(element.text)),
|
: InlineCustomWidget(child: MentionChip(element.text)),
|
||||||
|
|
||||||
"img" =>
|
"img" =>
|
||||||
src == null
|
src == null
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: InlineCustomWidget(
|
: InlineCustomWidget(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
child: ExpandableImage(
|
child: ExpandableImage(
|
||||||
src,
|
src,
|
||||||
child: Image(
|
child: Image(
|
||||||
image: CachedNetworkImage(
|
image: CachedNetworkImage(
|
||||||
src,
|
src,
|
||||||
ref.watch(CrossCacheController.provider),
|
ref.watch(CrossCacheController.provider),
|
||||||
headers: ref.headers,
|
headers: ref.headers,
|
||||||
),
|
|
||||||
errorBuilder: (_, error, _) => Text(
|
|
||||||
"Image Failed to Load",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
),
|
||||||
|
errorBuilder: (_, error, _) => Text(
|
||||||
|
"Image Failed to Load",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: height.toDouble(),
|
||||||
|
width: width?.toDouble(),
|
||||||
|
loadingBuilder: (_, child, loadingProgress) =>
|
||||||
|
loadingProgress == null
|
||||||
|
? child
|
||||||
|
: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
height: height.toDouble(),
|
|
||||||
width: width?.toDouble(),
|
|
||||||
loadingBuilder: (_, child, loadingProgress) =>
|
|
||||||
loadingProgress == null
|
|
||||||
? child
|
|
||||||
: CircularProgressIndicator(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
("del" ||
|
||||||
("del" ||
|
"h1" ||
|
||||||
"h1" ||
|
"h2" ||
|
||||||
"h2" ||
|
"h3" ||
|
||||||
"h3" ||
|
"h4" ||
|
||||||
"h4" ||
|
"h5" ||
|
||||||
"h5" ||
|
"h6" ||
|
||||||
"h6" ||
|
"p" ||
|
||||||
"p" ||
|
"ul" ||
|
||||||
"ul" ||
|
"ol" ||
|
||||||
"ol" ||
|
"sup" ||
|
||||||
"sup" ||
|
"sub" ||
|
||||||
"sub" ||
|
"li" ||
|
||||||
"li" ||
|
"b" ||
|
||||||
"b" ||
|
"i" ||
|
||||||
"i" ||
|
"u" ||
|
||||||
"u" ||
|
"strong" ||
|
||||||
"strong" ||
|
"em" ||
|
||||||
"em" ||
|
"s" ||
|
||||||
"s" ||
|
"code" ||
|
||||||
"code" ||
|
"hr" ||
|
||||||
"hr" ||
|
"br" ||
|
||||||
"br" ||
|
"div" ||
|
||||||
"div" ||
|
"table" ||
|
||||||
"table" ||
|
"thead" ||
|
||||||
"thead" ||
|
"tbody" ||
|
||||||
"tbody" ||
|
"tr" ||
|
||||||
"tr" ||
|
"th" ||
|
||||||
"th" ||
|
"td" ||
|
||||||
"td" ||
|
"caption" ||
|
||||||
"caption" ||
|
"pre" ||
|
||||||
"pre" ||
|
"span" ||
|
||||||
"span" ||
|
"details" ||
|
||||||
"details" ||
|
"summary") =>
|
||||||
"summary") =>
|
null,
|
||||||
null,
|
|
||||||
|
|
||||||
_ => SizedBox.shrink(),
|
_ => SizedBox.shrink(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
customStylesBuilder: (element) => {
|
customStylesBuilder: (element) => {
|
||||||
"width": "auto",
|
"width": "auto",
|
||||||
...Map.fromEntries(
|
...Map.fromEntries(
|
||||||
element.attributes
|
element.attributes
|
||||||
.mapTo<MapEntry<String, String>?>(
|
.mapTo<MapEntry<String, String>?>(
|
||||||
(key, value) => switch (key) {
|
(key, value) => switch (key) {
|
||||||
"data-mx-color" => MapEntry("color", value),
|
"data-mx-color" => MapEntry("color", value),
|
||||||
"data-mx-bg-color" => MapEntry("background-color", value),
|
"data-mx-bg-color" => MapEntry("background-color", value),
|
||||||
_ => null,
|
_ => null,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.nonNulls,
|
.nonNulls,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
onTapUrl: (url) =>
|
onTapUrl: (url) =>
|
||||||
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
ref.watch(LaunchHelper.provider).launchUrl(Uri.parse(url)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/author_controller.dart";
|
import "package:nexus/controllers/author_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class MessageAvatar extends ConsumerWidget {
|
class MessageAvatar extends ConsumerWidget {
|
||||||
|
|
@ -14,10 +15,16 @@ class MessageAvatar extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) => ref
|
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||||
.watch(AuthorController.provider(message))
|
.watch(AuthorController.provider(message))
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (membership) => AvatarOrHash(
|
data: (membership) => InkWell(
|
||||||
membership.avatarUrl,
|
onTapDown: (details) => context.showUserPopover(
|
||||||
membership.displayName,
|
membership,
|
||||||
height: height,
|
globalPosition: details.globalPosition,
|
||||||
|
),
|
||||||
|
child: AvatarOrHash(
|
||||||
|
membership.avatarUrl,
|
||||||
|
membership.displayName,
|
||||||
|
height: height,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
loading: () =>
|
loading: () =>
|
||||||
AvatarOrHash(null, message.authorId.substring(1), height: height),
|
AvatarOrHash(null, message.authorId.substring(1), height: height),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import "package:flutter/widgets.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat_core/flutter_chat_core.dart";
|
import "package:flutter_chat_core/flutter_chat_core.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
import "package:nexus/controllers/author_controller.dart";
|
import "package:nexus/controllers/author_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
|
|
||||||
class MessageDisplayname extends ConsumerWidget {
|
class MessageDisplayname extends ConsumerWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
@ -13,10 +14,16 @@ class MessageDisplayname extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) => ref
|
Widget build(BuildContext context, WidgetRef ref) => ref
|
||||||
.watch(AuthorController.provider(message))
|
.watch(AuthorController.provider(message))
|
||||||
.betterWhen(
|
.betterWhen(
|
||||||
data: (membership) => Text(
|
data: (membership) => InkWell(
|
||||||
"${membership.displayName}${message.metadata?["pmp"] == null ? "" : " (via ${message.authorId})"}",
|
onTapDown: (details) => context.showUserPopover(
|
||||||
style: style,
|
membership,
|
||||||
overflow: TextOverflow.ellipsis,
|
globalPosition: details.globalPosition,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"${membership.displayName}${message.metadata?["pmp"] == null ? "" : " (via ${message.authorId})"}",
|
||||||
|
style: style,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
loading: () => Text(""),
|
loading: () => Text(""),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import "package:flutter/material.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
import "package:nexus/controllers/members_controller.dart";
|
import "package:nexus/controllers/members_controller.dart";
|
||||||
import "package:nexus/helpers/extensions/better_when.dart";
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
class MemberList extends ConsumerWidget {
|
class MemberList extends ConsumerWidget {
|
||||||
|
|
@ -35,23 +36,24 @@ class MemberList extends ConsumerWidget {
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: members
|
children: members
|
||||||
.map(
|
.map(
|
||||||
(member) => ListTile(
|
(member) => InkWell(
|
||||||
onTap: () => showDialog(
|
onTapDown: (details) => context.showUserPopover(
|
||||||
context: context,
|
member,
|
||||||
builder: (context) =>
|
globalPosition: details.globalPosition,
|
||||||
Dialog(child: Text("TODO: Open member popover")),
|
|
||||||
),
|
),
|
||||||
leading: AvatarOrHash(
|
child: ListTile(
|
||||||
member.avatarUrl,
|
leading: AvatarOrHash(
|
||||||
member.displayName,
|
member.avatarUrl,
|
||||||
),
|
member.displayName,
|
||||||
title: Text(
|
),
|
||||||
member.displayName,
|
title: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
member.displayName,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
subtitle: Text(
|
),
|
||||||
member.userId,
|
subtitle: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
member.userId,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
91
lib/widgets/chat_page/user_popover.dart
Normal file
91
lib/widgets/chat_page/user_popover.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import "package:flutter/material.dart";
|
||||||
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/profile_controller.dart";
|
||||||
|
import "package:nexus/helpers/extensions/better_when.dart";
|
||||||
|
import "package:nexus/models/membership.dart";
|
||||||
|
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||||
|
|
||||||
|
class UserPopover extends ConsumerWidget {
|
||||||
|
final Membership member;
|
||||||
|
const UserPopover(this.member, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final textTheme = theme.textTheme;
|
||||||
|
return IntrinsicWidth(
|
||||||
|
child: Column(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
spacing: 16,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AvatarOrHash(member.avatarUrl, member.displayName, height: 80),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(
|
||||||
|
member.displayName,
|
||||||
|
style: textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
SelectableText(member.userId, style: textTheme.titleSmall),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
ref
|
||||||
|
.watch(ProfileController.provider(member.userId))
|
||||||
|
.betterWhen(
|
||||||
|
loading: SizedBox.shrink,
|
||||||
|
data: (profile) => Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
for (final pronoun in profile.pronouns.where(
|
||||||
|
(pronoun) => pronoun.language == "en",
|
||||||
|
))
|
||||||
|
Chip(label: Text(pronoun.summary)),
|
||||||
|
if (profile.timezone != null)
|
||||||
|
Chip(label: Text(profile.timezone!)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
FilledButton.icon(onPressed: null, label: Text("Message")),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: null,
|
||||||
|
label: Text("Kick"),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
foregroundColor: WidgetStatePropertyAll(
|
||||||
|
theme.colorScheme.onError,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: null,
|
||||||
|
label: Text("Ban"),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: WidgetStatePropertyAll(
|
||||||
|
theme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
foregroundColor: WidgetStatePropertyAll(
|
||||||
|
theme.colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
].map((e) => Expanded(child: e)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -490,11 +490,10 @@ packages:
|
||||||
flutter_link_previewer:
|
flutter_link_previewer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/flutter_link_previewer"
|
name: flutter_link_previewer
|
||||||
ref: HEAD
|
sha256: "346f345064e65bc8bf739bccf19d6d6ca50f8183ffc52e452afa58c06ee2cbf7"
|
||||||
resolved-ref: "03be67c8c81c8f637672ee03dd8f082d2c223627"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/Henry-Hiles/flutter_chat_ui"
|
source: hosted
|
||||||
source: git
|
|
||||||
version: "4.2.0"
|
version: "4.2.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue