import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.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:markdown_widget/markdown_widget.dart'; import 'package:url_launcher/url_launcher.dart'; import 'image.dart'; class MarkdownTextContent extends HookConsumerWidget { final String content; final bool isAutoWarp; final TextScaler? textScaler; final TextStyle? textStyle; final TextStyle? linkStyle; final bool isSelectable; const MarkdownTextContent({ super.key, required this.content, this.isAutoWarp = false, this.textScaler, this.textStyle, this.linkStyle, this.isSelectable = false, }); @override Widget build(BuildContext context, WidgetRef ref) { final baseUrl = ref.watch(serverUrlProvider); final doesEnlargeSticker = useMemoized(() { // Check if content only contains one sticker by matching the sticker pattern final stickerPattern = RegExp(r':([-\w]+):'); final matches = stickerPattern.allMatches(content); // Content should only contain one sticker and nothing else (except whitespace) final contentWithoutStickers = content.replaceAll(stickerPattern, '').trim(); return matches.length == 1 && contentWithoutStickers.isEmpty; }, [content]); final isDark = Theme.of(context).brightness == Brightness.dark; final config = isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig; return MarkdownBlock( data: content, 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!, ), 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); }, ), ); } }, ), 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, ); return content; }, ), ], ), generator: MarkdownGenerator( inlineSyntaxList: [_UserNameCardInlineSyntax(), _StickerInlineSyntax()], linesMargin: EdgeInsets.zero, ), ); } } class _UserNameCardInlineSyntax extends markdown.InlineSyntax { _UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+'); @override bool onMatch(markdown.InlineParser parser, Match match) { final alias = match[0]!; final anchor = markdown.Element.text('a', alias) ..attributes['href'] = Uri.encodeFull( 'solian://account/${alias.substring(1)}', ); parser.addNode(anchor); return true; } } class _StickerInlineSyntax extends markdown.InlineSyntax { _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('solian://stickers/$placeholder'); parser.addNode(image); return true; } }