♻️ Refactored markdown content
This commit is contained in:
@@ -115,7 +115,7 @@ class AccountStatusWidget extends HookConsumerWidget {
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
if (userStatus.value!.isOnline)
|
||||
if (userStatus.value?.isOnline ?? false)
|
||||
Icon(
|
||||
Symbols.circle,
|
||||
fill: 1,
|
||||
|
@@ -6,8 +6,16 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
export 'content/alert.native.dart'
|
||||
if (dart.library.html) 'content/alert.web.dart';
|
||||
|
||||
void showSnackBar(BuildContext context, String message) {
|
||||
showSnackBar(context, message);
|
||||
void showSnackBar(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
showSnackBar(context, message, action: action);
|
||||
}
|
||||
|
||||
void clearSnackBar(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
}
|
||||
|
||||
OverlayEntry? _loadingOverlay;
|
||||
|
@@ -4,29 +4,29 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
|
||||
import '../../screens/chat/room.dart';
|
||||
|
||||
class MessageBubbleAction {
|
||||
class MessageItemAction {
|
||||
static const String edit = "edit";
|
||||
static const String delete = "delete";
|
||||
static const String reply = "reply";
|
||||
static const String forward = "forward";
|
||||
}
|
||||
|
||||
class MessageBubble extends HookConsumerWidget {
|
||||
class MessageItem extends HookConsumerWidget {
|
||||
final LocalChatMessage message;
|
||||
final bool isCurrentUser;
|
||||
final Function(String action)? onAction;
|
||||
final Map<int, double>? progress;
|
||||
final bool showAvatar;
|
||||
|
||||
const MessageBubble({
|
||||
const MessageItem({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isCurrentUser,
|
||||
@@ -45,6 +45,10 @@ class MessageBubble extends HookConsumerWidget {
|
||||
isCurrentUser
|
||||
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
|
||||
: Theme.of(context).colorScheme.surfaceContainer;
|
||||
final linkColor = Color.alphaBlend(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
containerColor,
|
||||
);
|
||||
|
||||
final remoteMessage = message.toRemoteMessage();
|
||||
final sender = remoteMessage.sender;
|
||||
@@ -59,7 +63,7 @@ class MessageBubble extends HookConsumerWidget {
|
||||
title: 'edit'.tr(),
|
||||
image: MenuImage.icon(Symbols.edit),
|
||||
callback: () {
|
||||
onAction!.call(MessageBubbleAction.edit);
|
||||
onAction!.call(MessageItemAction.edit);
|
||||
},
|
||||
),
|
||||
if (isCurrentUser)
|
||||
@@ -67,7 +71,7 @@ class MessageBubble extends HookConsumerWidget {
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
onAction!.call(MessageBubbleAction.delete);
|
||||
onAction!.call(MessageItemAction.delete);
|
||||
},
|
||||
),
|
||||
if (isCurrentUser) MenuSeparator(),
|
||||
@@ -75,14 +79,14 @@ class MessageBubble extends HookConsumerWidget {
|
||||
title: 'reply'.tr(),
|
||||
image: MenuImage.icon(Symbols.reply),
|
||||
callback: () {
|
||||
onAction!.call(MessageBubbleAction.reply);
|
||||
onAction!.call(MessageItemAction.reply);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'forward'.tr(),
|
||||
image: MenuImage.icon(Symbols.forward),
|
||||
callback: () {
|
||||
onAction!.call(MessageBubbleAction.forward);
|
||||
onAction!.call(MessageItemAction.forward);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -93,27 +97,20 @@ class MessageBubble extends HookConsumerWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
isCurrentUser
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showAvatar && !isCurrentUser) ...[
|
||||
if (showAvatar) ...[
|
||||
Row(
|
||||
spacing: 8,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isCurrentUser)
|
||||
ProfilePictureWidget(
|
||||
fileId: sender.account.profile.pictureId,
|
||||
radius: 18,
|
||||
),
|
||||
ProfilePictureWidget(
|
||||
fileId: sender.account.profile.pictureId,
|
||||
radius: 16,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
isCurrentUser
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 2,
|
||||
children: [
|
||||
Text(
|
||||
@@ -124,41 +121,24 @@ class MessageBubble extends HookConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 5,
|
||||
children: [
|
||||
if (isCurrentUser)
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
Text(
|
||||
sender.account.nick,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (!isCurrentUser)
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
Badge(
|
||||
label:
|
||||
Text(
|
||||
sender.role >= 100
|
||||
? 'permissionOwner'
|
||||
: sender.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isCurrentUser)
|
||||
ProfilePictureWidget(
|
||||
fileId: sender.account.profile.pictureId,
|
||||
radius: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(4),
|
||||
@@ -169,14 +149,6 @@ class MessageBubble extends HookConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (isCurrentUser)
|
||||
_buildMessageIndicators(
|
||||
context,
|
||||
textColor,
|
||||
remoteMessage,
|
||||
message,
|
||||
isCurrentUser,
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -203,9 +175,14 @@ class MessageBubble extends HookConsumerWidget {
|
||||
isReply: false,
|
||||
),
|
||||
if (remoteMessage.content?.isNotEmpty ?? false)
|
||||
Text(
|
||||
remoteMessage.content!,
|
||||
style: TextStyle(color: textColor),
|
||||
MarkdownTextContent(
|
||||
content: remoteMessage.content!,
|
||||
isSelectable: true,
|
||||
linkStyle: TextStyle(color: linkColor),
|
||||
textStyle: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (remoteMessage.attachments.isNotEmpty)
|
||||
CloudFileList(
|
||||
@@ -260,14 +237,13 @@ class MessageBubble extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isCurrentUser)
|
||||
_buildMessageIndicators(
|
||||
context,
|
||||
textColor,
|
||||
remoteMessage,
|
||||
message,
|
||||
isCurrentUser,
|
||||
),
|
||||
_buildMessageIndicators(
|
||||
context,
|
||||
textColor,
|
||||
remoteMessage,
|
||||
message,
|
||||
isCurrentUser,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -288,10 +264,6 @@ class MessageBubble extends HookConsumerWidget {
|
||||
spacing: 4,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat.Hm().format(message.createdAt.toLocal()),
|
||||
style: TextStyle(fontSize: 11, color: textColor.withOpacity(0.7)),
|
||||
),
|
||||
if (remoteMessage.editedAt != null)
|
||||
Text(
|
||||
'edited'.tr().toLowerCase(),
|
@@ -183,47 +183,75 @@ class SplitAvatarWidget extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: _buildQuadrant(context, filesId[0], ref, radius),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: _buildQuadrant(context, filesId[1], ref, radius),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
child: _buildQuadrant(context, filesId[2], ref, radius),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child:
|
||||
filesId.length > 4
|
||||
? Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${filesId.length - 3}',
|
||||
style: TextStyle(
|
||||
fontSize: radius * 0.4,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[0],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
)
|
||||
: _buildQuadrant(context, filesId[3], ref, radius),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[1],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuadrant(
|
||||
context,
|
||||
filesId[2],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
filesId.length > 4
|
||||
? Container(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+${filesId.length - 3}',
|
||||
style: TextStyle(
|
||||
fontSize: radius * 0.4,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildQuadrant(
|
||||
context,
|
||||
filesId[3],
|
||||
ref,
|
||||
radius,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_highlight/flutter_highlight.dart';
|
||||
import 'package:flutter_highlight/theme_map.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:markdown/markdown.dart' as markdown;
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:markdown_widget/markdown_widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'image.dart';
|
||||
|
||||
@@ -16,14 +16,18 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
final String content;
|
||||
final bool isAutoWarp;
|
||||
final TextScaler? textScaler;
|
||||
final Color? textColor;
|
||||
final TextStyle? textStyle;
|
||||
final TextStyle? linkStyle;
|
||||
final bool isSelectable;
|
||||
|
||||
const MarkdownTextContent({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.isAutoWarp = false,
|
||||
this.textScaler,
|
||||
this.textColor,
|
||||
this.textStyle,
|
||||
this.linkStyle,
|
||||
this.isSelectable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -40,91 +44,111 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
return matches.length == 1 && contentWithoutStickers.isEmpty;
|
||||
}, [content]);
|
||||
|
||||
return Markdown(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final config =
|
||||
isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig;
|
||||
|
||||
return MarkdownBlock(
|
||||
data: content,
|
||||
padding: EdgeInsets.zero,
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(
|
||||
textScaler: textScaler,
|
||||
p:
|
||||
textColor != null
|
||||
? Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium!.copyWith(color: textColor)
|
||||
: null,
|
||||
blockquote: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
blockquoteDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
horizontalRuleDecoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(width: 1.0, color: Theme.of(context).dividerColor),
|
||||
selectable: isSelectable,
|
||||
config: config.copy(
|
||||
configs: [
|
||||
isDark
|
||||
? PreConfig.darkConfig.copy(
|
||||
textStyle: textStyle,
|
||||
padding: EdgeInsets.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
)
|
||||
: PreConfig().copy(
|
||||
textStyle: textStyle,
|
||||
padding: EdgeInsets.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
PConfig(
|
||||
textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
),
|
||||
codeblockDecoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor, width: 0.3),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||
),
|
||||
code: GoogleFonts.robotoMono(height: 1),
|
||||
),
|
||||
builders: {'latex': LatexElementBuilder(), 'code': HighlightBuilder()},
|
||||
softLineBreak: true,
|
||||
extensionSet: markdown.ExtensionSet(
|
||||
<markdown.BlockSyntax>[
|
||||
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||
markdown.CodeBlockSyntax(),
|
||||
markdown.FencedCodeBlockSyntax(),
|
||||
LatexBlockSyntax(),
|
||||
],
|
||||
<markdown.InlineSyntax>[
|
||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
|
||||
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||
_UserNameCardInlineSyntax(),
|
||||
_StickerInlineSyntax(ref.read(serverUrlProvider)),
|
||||
markdown.AutolinkSyntax(),
|
||||
markdown.AutolinkExtensionSyntax(),
|
||||
markdown.CodeSyntax(),
|
||||
LatexInlineSyntax(),
|
||||
],
|
||||
),
|
||||
onTapLink: (text, href, title) async {
|
||||
if (href == null) return;
|
||||
await launchUrlString(href, mode: LaunchMode.externalApplication);
|
||||
},
|
||||
imageBuilder: (uri, title, alt) {
|
||||
if (uri.scheme == 'solink') {
|
||||
switch (uri.host) {
|
||||
case 'stickers':
|
||||
final size = doesEnlargeSticker ? 96.0 : 24.0;
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
LinkConfig(
|
||||
style:
|
||||
linkStyle ??
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
onTap: (herf) {
|
||||
final url = Uri.tryParse(herf);
|
||||
if (url != null) {
|
||||
if (url.scheme == 'solian') {
|
||||
context.router.pushPath(
|
||||
['', url.host, ...url.pathSegments].join('/'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final whitelistDomains = ['solian.app', 'solsynth.dev'];
|
||||
if (whitelistDomains.contains(url.host)) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
return;
|
||||
}
|
||||
showConfirmAlert(
|
||||
'openLinkConfirmDescription'.tr(args: [url.toString()]),
|
||||
'openLinkConfirm'.tr(),
|
||||
).then((value) {
|
||||
if (value) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showSnackBar(
|
||||
context,
|
||||
'brokenLink'.tr(args: [herf]),
|
||||
action: SnackBarAction(
|
||||
label: 'copyToClipboard'.tr(),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: herf));
|
||||
clearSnackBar(context);
|
||||
},
|
||||
),
|
||||
child: UniversalImage(
|
||||
uri: '$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open',
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
noCacheOptimization: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ImgConfig(
|
||||
builder: (url, attributes) {
|
||||
final uri = Uri.parse(url);
|
||||
if (uri.scheme == 'solian') {
|
||||
switch (uri.host) {
|
||||
case 'stickers':
|
||||
final size = doesEnlargeSticker ? 96.0 : 24.0;
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: UniversalImage(
|
||||
uri:
|
||||
'$baseUrl/stickers/lookup/${uri.pathSegments[0]}/open',
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
noCacheOptimization: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
final content = UniversalImage(
|
||||
uri: uri.toString(),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
}
|
||||
}
|
||||
final content = UniversalImage(uri: uri.toString(), fit: BoxFit.cover);
|
||||
if (alt != null) {
|
||||
return Tooltip(message: alt, child: content);
|
||||
}
|
||||
return content;
|
||||
},
|
||||
return content;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
generator: MarkdownGenerator(
|
||||
inlineSyntaxList: [_UserNameCardInlineSyntax(), _StickerInlineSyntax()],
|
||||
linesMargin: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -137,7 +161,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
||||
final alias = match[0]!;
|
||||
final anchor = markdown.Element.text('a', alias)
|
||||
..attributes['href'] = Uri.encodeFull(
|
||||
'solink://accounts/${alias.substring(1)}',
|
||||
'solian://account/${alias.substring(1)}',
|
||||
);
|
||||
parser.addNode(anchor);
|
||||
|
||||
@@ -146,72 +170,15 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
||||
}
|
||||
|
||||
class _StickerInlineSyntax extends markdown.InlineSyntax {
|
||||
final String baseUrl;
|
||||
_StickerInlineSyntax(this.baseUrl) : super(r':([-\w]+):');
|
||||
_StickerInlineSyntax() : super(r':([-\w]+):');
|
||||
|
||||
@override
|
||||
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||
final placeholder = match[1]!;
|
||||
final image = markdown.Element.text('img', '')
|
||||
..attributes['src'] = Uri.encodeFull('solink://stickers/$placeholder');
|
||||
..attributes['src'] = Uri.encodeFull('solian://stickers/$placeholder');
|
||||
parser.addNode(image);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class HighlightBuilder extends MarkdownElementBuilder {
|
||||
@override
|
||||
Widget? visitElementAfterWithContext(
|
||||
BuildContext context,
|
||||
markdown.Element element,
|
||||
TextStyle? preferredStyle,
|
||||
TextStyle? parentStyle,
|
||||
) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
if (element.attributes['class'] == null &&
|
||||
!element.textContent.trim().contains('\n')) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0),
|
||||
margin: EdgeInsets.symmetric(horizontal: 2.0),
|
||||
color: Colors.black12,
|
||||
child: Text(
|
||||
element.textContent,
|
||||
style: GoogleFonts.robotoMono(textStyle: preferredStyle),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
var language = 'plaintext';
|
||||
final pattern = RegExp(r'^language-(.+)$');
|
||||
if (element.attributes['class'] != null &&
|
||||
pattern.hasMatch(element.attributes['class'] ?? '')) {
|
||||
language =
|
||||
pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ??
|
||||
'plaintext';
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: HighlightView(
|
||||
element.textContent.trim(),
|
||||
language: language,
|
||||
theme: {
|
||||
...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!),
|
||||
'root':
|
||||
(isDark
|
||||
? TextStyle(
|
||||
backgroundColor: Colors.transparent,
|
||||
color: Color(0xfff8f8f2),
|
||||
)
|
||||
: TextStyle(
|
||||
backgroundColor: Colors.transparent,
|
||||
color: Color(0xff545454),
|
||||
)),
|
||||
},
|
||||
padding: EdgeInsets.all(12),
|
||||
textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user