Rendering stickers inside content

This commit is contained in:
2025-05-11 23:28:04 +08:00
parent e61497dc70
commit 8efd8cd58e
12 changed files with 117 additions and 103 deletions

View File

@ -7,6 +7,8 @@ import 'package:flutter_platform_alert/flutter_platform_alert.dart';
import 'package:gap/gap.dart';
import 'package:styled_widget/styled_widget.dart';
// TODO support web here
String _parseRemoteError(DioException err) {
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
if (err.response?.data is String) return err.response?.data;

View File

@ -6,21 +6,48 @@ class UniversalImage extends StatelessWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
final double? width;
final double? height;
final bool noCacheOptimization;
const UniversalImage({
super.key,
required this.uri,
this.blurHash,
this.fit = BoxFit.cover,
this.width,
this.height,
this.noCacheOptimization = false,
});
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
if (blurHash != null) BlurHash(hash: blurHash!),
CachedNetworkImage(imageUrl: uri, fit: fit),
],
int? cacheWidth;
int? cacheHeight;
if (width != null && height != null && !noCacheOptimization) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
cacheWidth = width != null ? (width! * devicePixelRatio).round() : null;
cacheHeight =
height != null ? (height! * devicePixelRatio).round() : null;
}
return SizedBox(
width: width,
height: height,
child: Stack(
fit: StackFit.expand,
children: [
if (blurHash != null) BlurHash(hash: blurHash!),
CachedNetworkImage(
imageUrl: uri,
fit: fit,
width: width,
height: height,
memCacheHeight: cacheHeight,
memCacheWidth: cacheWidth,
),
],
),
);
}
}

View File

@ -5,11 +5,16 @@ class UniversalImage extends StatelessWidget {
final String uri;
final String? blurHash;
final BoxFit fit;
final double? width;
final double? height;
const UniversalImage({
super.key,
required this.uri,
this.blurHash,
this.fit = BoxFit.cover,
this.width,
this.height,
});
@override
@ -19,8 +24,8 @@ class UniversalImage extends StatelessWidget {
onElementCreated: (element) {
element as web.HTMLImageElement;
element.src = uri;
element.style.width = '100%';
element.style.height = '100%';
element.style.width = width?.toString() ?? '100%';
element.style.height = height?.toString() ?? '100%';
element.style.objectFit = switch (fit) {
BoxFit.cover || BoxFit.fitWidth || BoxFit.fitHeight => 'cover',
BoxFit.fill => 'fill',

View File

@ -1,16 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/theme_map.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:markdown/markdown.dart' as markdown;
import 'package:url_launcher/url_launcher_string.dart';
class MarkdownTextContent extends StatelessWidget {
import 'image.dart';
class MarkdownTextContent extends HookConsumerWidget {
final String content;
final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler;
final Color? textColor;
@ -18,13 +22,24 @@ class MarkdownTextContent extends StatelessWidget {
super.key,
required this.content,
this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler,
this.textColor,
});
@override
Widget build(BuildContext context) {
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]);
return Markdown(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -70,6 +85,7 @@ class MarkdownTextContent extends StatelessWidget {
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(ref.read(serverUrlProvider)),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
@ -80,6 +96,35 @@ class MarkdownTextContent extends StatelessWidget {
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)),
),
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);
if (alt != null) {
return Tooltip(message: alt, child: content);
}
return content;
},
);
}
}
@ -100,6 +145,21 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
}
}
class _StickerInlineSyntax extends markdown.InlineSyntax {
final String baseUrl;
_StickerInlineSyntax(this.baseUrl) : 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');
parser.addNode(image);
return true;
}
}
class HighlightBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfterWithContext(