diff --git a/lib/widgets/chat/chat_message.dart b/lib/widgets/chat/chat_message.dart index 1ed4b0c..d0d2f07 100644 --- a/lib/widgets/chat/chat_message.dart +++ b/lib/widgets/chat/chat_message.dart @@ -297,6 +297,8 @@ class _ChatMessageText extends StatelessWidget { child: MarkdownTextContent( content: data.body['text'], isAutoWarp: true, + isEnlargeSticker: + RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''), ), ), ), diff --git a/lib/widgets/markdown_content.dart b/lib/widgets/markdown_content.dart index 2a23739..218683d 100644 --- a/lib/widgets/markdown_content.dart +++ b/lib/widgets/markdown_content.dart @@ -44,7 +44,9 @@ class MarkdownTextContent extends StatelessWidget { Theme.of(context), ).copyWith( textScaler: textScaler, - p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null, + p: textColor != null + ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) + : null, blockquote: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, ), @@ -115,7 +117,7 @@ class MarkdownTextContent extends StatelessWidget { final alias = segments[1]; final st = context.read(); final sn = context.read(); - final double size = isEnlargeSticker ? 128 : 32; + final double size = isEnlargeSticker ? 96 : 32; return Container( width: size, height: size, @@ -131,7 +133,8 @@ class MarkdownTextContent extends StatelessWidget { if (snapshot.hasData) { return GestureDetector( child: UniversalImage( - sn.getAttachmentUrl(snapshot.data!.attachment.rid), + sn.getAttachmentUrl( + snapshot.data!.attachment.rid), fit: BoxFit.contain, width: size, height: size, @@ -177,7 +180,9 @@ class MarkdownTextContent extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(8)), child: AspectRatio( aspectRatio: attachment.metadata['ratio'] ?? - switch (attachment.mimetype.split('/').firstOrNull) { + switch (attachment.mimetype + .split('/') + .firstOrNull) { 'audio' => 16 / 9, 'video' => 16 / 9, _ => 1, diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 49a6763..981933d 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -92,9 +92,10 @@ class OpenablePostItem extends StatelessWidget { openColor: Colors.transparent, openElevation: 0, transitionType: ContainerTransitionType.fade, - closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( - cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, - ), + closedColor: + Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( + cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, + ), closedShape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), @@ -135,9 +136,11 @@ class PostItem extends StatelessWidget { final box = context.findRenderObject() as RenderBox?; final url = 'https://solsynth.dev/posts/${data.id}'; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { - Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + Share.shareUri(Uri.parse(url), + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); } else { - Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); + Share.share(url, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size); } } @@ -155,7 +158,8 @@ class PostItem extends StatelessWidget { child: MultiProvider( providers: [ Provider(create: (_) => context.read()), - ChangeNotifierProvider(create: (_) => context.read()), + ChangeNotifierProvider( + create: (_) => context.read()), ], child: ResponsiveBreakpoints.builder( breakpoints: ResponsiveBreakpoints.of(context).breakpoints, @@ -183,7 +187,8 @@ class PostItem extends StatelessWidget { sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ); } else { - await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile); + await FileSaver.instance.saveFile( + name: 'Solar Network Post #${data.id}.png', file: imageFile); } await imageFile.delete(); @@ -197,7 +202,9 @@ class PostItem extends StatelessWidget { final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id; // Video full view - if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) { + if (showFullPost && + data.type == 'video' && + ResponsiveBreakpoints.of(context).largerThan(TABLET)) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -217,7 +224,8 @@ class PostItem extends StatelessWidget { if (onDeleted != null) {} }, ).padding(bottom: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8), + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(bottom: 8), _PostHeadline(data: data).padding(horizontal: 4, bottom: 8), _PostFeaturedComment(data: data), _PostBottomAction( @@ -265,7 +273,8 @@ class PostItem extends StatelessWidget { if (onDeleted != null) {} }, ).padding(horizontal: 12, top: 8, bottom: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12), @@ -308,8 +317,13 @@ class PostItem extends StatelessWidget { ], ), ), - Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), - _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), + Text('postArticle') + .tr() + .fontSize(13) + .opacity(0.75) + .padding(horizontal: 24, bottom: 8), + _PostFeaturedComment(data: data, maxWidth: maxWidth) + .padding(horizontal: 12), _PostBottomAction( data: data, showComments: showComments, @@ -324,7 +338,8 @@ class PostItem extends StatelessWidget { } final displayableAttachments = data.preload?.attachments - ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') + ?.where((ele) => + ele?.mediaType != SnMediaType.image || data.type != 'article') .toList(); final cfg = context.read(); @@ -349,9 +364,13 @@ class PostItem extends StatelessWidget { if (onDeleted != null) onDeleted!(); }, ).padding(horizontal: 12, vertical: 8), - if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), - if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), - if (data.body['title'] != null || data.body['description'] != null) + if (data.preload?.video != null) + _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8), + if (data.type == 'question') + _PostQuestionHint(data: data) + .padding(horizontal: 16, bottom: 8), + if (data.body['title'] != null || + data.body['description'] != null) _PostHeadline( data: data, isEnlarge: data.type == 'article' && showFullPost, @@ -365,7 +384,8 @@ class PostItem extends StatelessWidget { if (data.repostTo != null) _PostQuoteContent(child: data.repostTo!).padding( horizontal: 12, - bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0, + bottom: + data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0, ), if (data.visibility > 0) _PostVisibilityHint(data: data).padding( @@ -377,7 +397,9 @@ class PostItem extends StatelessWidget { horizontal: 16, vertical: 4, ), - if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6), + if (data.tags.isNotEmpty) + _PostTagsList(data: data) + .padding(horizontal: 16, top: 4, bottom: 6), ], ), ), @@ -390,12 +412,16 @@ class PostItem extends StatelessWidget { fit: showFullPost ? BoxFit.cover : BoxFit.contain, padding: const EdgeInsets.symmetric(horizontal: 12), ), - if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4), - if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) + if (data.preload?.poll != null) + PostPoll(poll: data.preload!.poll!) + .padding(horizontal: 12, vertical: 4), + if (data.body['content'] != null && + (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) LinkPreviewWidget( text: data.body['content'], ).padding(horizontal: 4), - _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), + _PostFeaturedComment(data: data, maxWidth: maxWidth) + .padding(horizontal: 12), Container( constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), child: Column( @@ -457,7 +483,8 @@ class PostShareImageWidget extends StatelessWidget { showMenu: false, isRelativeDate: false, ).padding(horizontal: 16, bottom: 8), - if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), + if (data.type == 'question') + _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8), _PostHeadline( data: data, isEnlarge: data.type == 'article', @@ -472,7 +499,8 @@ class PostShareImageWidget extends StatelessWidget { child: data.repostTo!, isRelativeDate: false, ).padding(horizontal: 16, bottom: 8), - if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) + if (data.type != 'article' && + (data.preload?.attachments?.isNotEmpty ?? false)) StyledWidget(AttachmentList( data: data.preload!.attachments!, columned: true, @@ -481,7 +509,8 @@ class PostShareImageWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (data.visibility > 0) _PostVisibilityHint(data: data), - if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data), + if (data.body['content_truncated'] == true) + _PostTruncatedHint(data: data), ], ).padding(horizontal: 16), _PostBottomAction( @@ -541,7 +570,8 @@ class PostShareImageWidget extends StatelessWidget { version: QrVersions.auto, size: 100, gapless: true, - embeddedImage: AssetImage('assets/icon/icon-light-radius.png'), + embeddedImage: + AssetImage('assets/icon/icon-light-radius.png'), embeddedImageStyle: QrEmbeddedImageStyle( size: Size(28, 28), ), @@ -572,9 +602,11 @@ class _PostQuestionHint extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20), + Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, + size: 20), const Gap(4), - if (data.body['answer'] == null && data.body['reward']?.toDouble() != null) + if (data.body['answer'] == null && + data.body['reward']?.toDouble() != null) Text('postQuestionUnansweredWithReward'.tr(args: [ '${data.body['reward']}', ])).opacity(0.75) @@ -610,7 +642,9 @@ class _PostBottomAction extends StatelessWidget { ); final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty - ? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key + ? data.metric.reactionList.entries + .reduce((a, b) => a.value > b.value ? a : b) + .key : null; return Row( @@ -624,7 +658,8 @@ class _PostBottomAction extends StatelessWidget { InkWell( child: Row( children: [ - if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null) + if (mostTypicalReaction == null || + kTemplateReactions[mostTypicalReaction] == null) Icon(Symbols.add_reaction, size: 20, color: iconColor) else Text( @@ -636,7 +671,8 @@ class _PostBottomAction extends StatelessWidget { ), ), const Gap(8), - if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote) + if (data.totalUpvote > 0 && + data.totalUpvote >= data.totalDownvote) Text('postReactionUpvote').plural( data.totalUpvote, ) @@ -655,8 +691,12 @@ class _PostBottomAction extends StatelessWidget { data: data, onChanged: (value, attr, delta) { onChanged(data.copyWith( - totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote, - totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote, + totalUpvote: attr == 1 + ? data.totalUpvote + delta + : data.totalUpvote, + totalDownvote: attr == 2 + ? data.totalDownvote + delta + : data.totalDownvote, metric: data.metric.copyWith(reactionList: value), )); }, @@ -904,8 +944,10 @@ class _PostContentHeader extends StatelessWidget { const Gap(4), Text( isRelativeDate - ? RelativeTime(context).format(data.publishedAt ?? data.createdAt) - : DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt), + ? RelativeTime(context) + .format(data.publishedAt ?? data.createdAt) + : DateFormat('y/M/d HH:mm') + .format(data.publishedAt ?? data.createdAt), ).fontSize(13), ], ).opacity(0.8), @@ -923,8 +965,10 @@ class _PostContentHeader extends StatelessWidget { const Gap(4), Text( isRelativeDate - ? RelativeTime(context).format(data.publishedAt ?? data.createdAt) - : DateFormat('y/M/d HH:mm').format(data.publishedAt ?? data.createdAt), + ? RelativeTime(context) + .format(data.publishedAt ?? data.createdAt) + : DateFormat('y/M/d HH:mm') + .format(data.publishedAt ?? data.createdAt), ).fontSize(13), ], ).opacity(0.8), @@ -1107,7 +1151,8 @@ class _PostContentBody extends StatelessWidget { if (data.body['content'] == null) return const SizedBox.shrink(); final content = MarkdownTextContent( isAutoWarp: data.type == 'story', - isEnlargeSticker: true, + isEnlargeSticker: + RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''), textScaler: isEnlarge ? TextScaler.linear(1.1) : null, content: data.body['content'], attachments: data.preload?.attachments, @@ -1156,10 +1201,12 @@ class _PostQuoteContent extends StatelessWidget { onDeleted: () {}, ).padding(bottom: 4), _PostContentBody(data: child), - if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4), + if (child.visibility > 0) + _PostVisibilityHint(data: child).padding(top: 4), ], ).padding(horizontal: 16), - if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false)) + if (child.type != 'article' && + (child.preload?.attachments?.isNotEmpty ?? false)) ClipRRect( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(8), @@ -1310,7 +1357,9 @@ class _PostTruncatedHint extends StatelessWidget { const Gap(4), Text('postReadEstimate').tr(args: [ '${Duration( - seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed, + seconds: (data.body['content_length'] as num).toDouble() * + 60 ~/ + kHumanReadSpeed, ).inSeconds}s', ]), ], @@ -1349,7 +1398,8 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { // If this is a answered question, fetch the answer instead if (widget.data.type == 'question' && widget.data.body['answer'] != null) { final sn = context.read(); - final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}'); + final resp = + await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}'); _isAnswer = true; setState(() => _featuredComment = SnPost.fromJson(resp.data)); return; @@ -1357,9 +1407,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { try { final sn = context.read(); - final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: { - 'take': 1, - }); + final resp = await sn.client.get( + '/cgi/co/posts/${widget.data.id}/replies/featured', + queryParameters: { + 'take': 1, + }); setState(() => _featuredComment = SnPost.fromJson(resp.data[0])); } catch (err) { if (!mounted) return; @@ -1388,7 +1440,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { width: double.infinity, child: Material( borderRadius: const BorderRadius.all(Radius.circular(8)), - color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh, + color: _isAnswer + ? Colors.green.withOpacity(0.5) + : Theme.of(context).colorScheme.surfaceContainerHigh, child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(8)), onTap: () { @@ -1408,11 +1462,17 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> { crossAxisAlignment: CrossAxisAlignment.center, children: [ const Gap(2), - Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20), + Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, + size: 20), const Gap(10), Text( - _isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment', - style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15), + _isAnswer + ? 'postQuestionAnswerTitle' + : 'postFeaturedComment', + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 15), ).tr(), ], ), @@ -1550,7 +1610,8 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { } RegExp cleanThinkingRegExp = RegExp(r'[\s\S]*?'); - setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); + setState( + () => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); } catch (err) { if (!mounted) return; context.showErrorDialog(err); @@ -1573,11 +1634,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { children: [ const Icon(Symbols.book_4_spark, size: 24), const Gap(16), - Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(), + Text('postGetInsightTitle', + style: Theme.of(context).textTheme.titleLarge) + .tr(), ], ).padding(horizontal: 20, top: 16, bottom: 12), const Gap(4), - Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20), + Text('postGetInsightDescription', + style: Theme.of(context).textTheme.bodySmall) + .tr() + .padding(horizontal: 20), const Gap(4), if (_response == null) Expanded( @@ -1595,12 +1661,16 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> { leading: const Icon(Symbols.info), title: Text('aiThinkingProcess'.tr()), tilePadding: const EdgeInsets.symmetric(horizontal: 20), - collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, + collapsedBackgroundColor: + Theme.of(context).colorScheme.surfaceContainerHigh, minTileHeight: 32, children: [ SelectableText( _thinkingProcess!, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(fontStyle: FontStyle.italic), ).padding(horizontal: 20, vertical: 8), ], ).padding(vertical: 8), @@ -1637,7 +1707,8 @@ class _PostVideoPlayer extends StatelessWidget { aspectRatio: 16 / 9, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'), + child: AttachmentItem( + data: data.preload!.video!, heroTag: 'post-video-${data.id}'), ), ), );