💄 New layout for article for optimized reading experience
🐛 Bug fixes on pending post media list
			
			
This commit is contained in:
		| @@ -383,5 +383,7 @@ | ||||
|   "accountStatusOnline": "Online", | ||||
|   "accountStatusOffline": "Offline", | ||||
|   "accountStatusLastSeen": "Last seen at {}", | ||||
|   "postArticle": "Article on the Solar Network" | ||||
|   "postArticle": "Article on the Solar Network", | ||||
|   "articleWrittenAt": "Written at {}", | ||||
|   "articleEditedAt": "Edited at {}" | ||||
| } | ||||
|   | ||||
| @@ -383,5 +383,8 @@ | ||||
|   "accountStatusOnline": "在线", | ||||
|   "accountStatusOffline": "离线", | ||||
|   "accountStatusLastSeen": "最后一次在 {} 上线", | ||||
|   "postArticle": "Solar Network 上的文章" | ||||
|   "postArticle": "Solar Network 上的文章", | ||||
|   "articleWrittenAt": "发表于 {}", | ||||
|   "articleEditedAt": "编辑于 {}" | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -264,6 +264,7 @@ class PostWriteController extends ChangeNotifier { | ||||
|  | ||||
|     final item = await _uploadAttachment(context, media); | ||||
|     attachments[idx] = PostWriteMedia(item); | ||||
|     isBusy = false; | ||||
|  | ||||
|     notifyListeners(); | ||||
|   } | ||||
| @@ -395,6 +396,9 @@ class PostWriteController extends ChangeNotifier { | ||||
|       attachments.add(thumbnail!); | ||||
|       thumbnail = null; | ||||
|     } else { | ||||
|       if (thumbnail != null) { | ||||
|         attachments.add(thumbnail!); | ||||
|       } | ||||
|       thumbnail = attachments[idx]; | ||||
|       attachments.removeAt(idx); | ||||
|     } | ||||
|   | ||||
| @@ -1,44 +1,48 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:markdown/markdown.dart' as markdown; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:syntax_highlight/syntax_highlight.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:path/path.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class MarkdownTextContent extends StatefulWidget { | ||||
| import 'attachment/attachment_detail.dart'; | ||||
|  | ||||
| class MarkdownTextContent extends StatelessWidget { | ||||
|   final String content; | ||||
|   final bool isSelectable; | ||||
|   final bool isLargeText; | ||||
|   final bool isAutoWarp; | ||||
|   final TextScaler? textScaler; | ||||
|   final List<SnAttachment?>? attachments; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
|     super.key, | ||||
|     required this.content, | ||||
|     this.isSelectable = false, | ||||
|     this.isLargeText = false, | ||||
|     this.isAutoWarp = false, | ||||
|     this.textScaler, | ||||
|     this.attachments, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<MarkdownTextContent> createState() => _MarkdownTextContentState(); | ||||
| } | ||||
|  | ||||
| class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|   Widget _buildContent(BuildContext context) { | ||||
|     return Markdown( | ||||
|       shrinkWrap: true, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
|       data: widget.content, | ||||
|       data: content, | ||||
|       padding: EdgeInsets.zero, | ||||
|       styleSheet: MarkdownStyleSheet.fromTheme( | ||||
|         Theme.of(context), | ||||
|       ).copyWith( | ||||
|           textScaler: TextScaler.linear(widget.isLargeText ? 1.1 : 1), | ||||
|           textScaler: textScaler, | ||||
|           blockquote: TextStyle( | ||||
|             color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|           ), | ||||
| @@ -73,7 +77,7 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|           ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|         ], | ||||
|         <markdown.InlineSyntax>[ | ||||
|           if (widget.isAutoWarp) markdown.LineBreakSyntax(), | ||||
|           if (isAutoWarp) markdown.LineBreakSyntax(), | ||||
|           _UserNameCardInlineSyntax(), | ||||
|           markdown.AutolinkSyntax(), | ||||
|           markdown.AutolinkExtensionSyntax(), | ||||
| @@ -85,7 +89,12 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|       onTapLink: (text, href, title) async { | ||||
|         if (href == null) return; | ||||
|         if (href.startsWith('solink://')) { | ||||
|           // final segments = href.replaceFirst('solink://', '').split('/'); | ||||
|           final uri = href.replaceFirst('solink://', ''); | ||||
|           final segments = uri.split('/'); | ||||
|           switch (segments[0]) { | ||||
|             default: | ||||
|               GoRouter.of(context).push(uri); | ||||
|           } | ||||
|           return; | ||||
|         } | ||||
|  | ||||
| @@ -99,7 +108,57 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|         double? width, height; | ||||
|         BoxFit? fit; | ||||
|         if (url.startsWith('solink://')) { | ||||
|           // final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|           final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|           switch (segments[0]) { | ||||
|             case 'attachments': | ||||
|               final attachment = attachments?.firstWhere( | ||||
|                 (ele) => ele?.rid == segments[1], | ||||
|                 orElse: () => null, | ||||
|               ); | ||||
|               if (attachment != null) { | ||||
|                 const uuid = Uuid(); | ||||
|                 final heroTag = uuid.v4(); | ||||
|                 return GestureDetector( | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       border: Border.all( | ||||
|                         color: Theme.of(context).dividerColor, | ||||
|                         width: 1, | ||||
|                       ), | ||||
|                     ), | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: attachment.metadata['ratio'] ?? | ||||
|                             switch (attachment.mimetype.split('/').firstOrNull) { | ||||
|                               'audio' => 16 / 9, | ||||
|                               'video' => 16 / 9, | ||||
|                               _ => 1, | ||||
|                             } | ||||
|                                 .toDouble(), | ||||
|                         child: AttachmentItem( | ||||
|                           data: attachment, | ||||
|                           heroTag: heroTag, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTap: () { | ||||
|                     context.pushTransparentRoute( | ||||
|                       AttachmentZoomView( | ||||
|                         data: [attachment], | ||||
|                         initialIndex: 0, | ||||
|                         heroTags: [heroTag], | ||||
|                       ), | ||||
|                       backgroundColor: Colors.black.withOpacity(0.7), | ||||
|                       rootNavigator: true, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|               } | ||||
|               break; | ||||
|           } | ||||
|           return const SizedBox.shrink(); | ||||
|         } | ||||
|         return UniversalImage( | ||||
| @@ -114,7 +173,7 @@ class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (widget.isSelectable) { | ||||
|     if (isSelectable) { | ||||
|       return SelectionArea(child: _buildContent(context)); | ||||
|     } | ||||
|     return _buildContent(context); | ||||
| @@ -153,13 +212,11 @@ class _MarkdownTextCodeElement extends MarkdownElementBuilder { | ||||
|       child: FutureBuilder( | ||||
|         future: (() async { | ||||
|           final docPath = '../../../'; | ||||
|           final highlightingPath = | ||||
|               join(docPath, 'assets/highlighting', language); | ||||
|           final highlightingPath = join(docPath, 'assets/highlighting', language); | ||||
|           await Highlighter.initialize([highlightingPath]); | ||||
|           return Highlighter( | ||||
|             language: highlightingPath, | ||||
|             theme: PlatformDispatcher.instance.platformBrightness == | ||||
|                     Brightness.light | ||||
|             theme: PlatformDispatcher.instance.platformBrightness == Brightness.light | ||||
|                 ? await HighlighterTheme.loadLightTheme() | ||||
|                 : await HighlighterTheme.loadDarkTheme(), | ||||
|           ); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import 'dart:math' as math; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:popover/popover.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -136,8 +137,14 @@ class PostItem extends StatelessWidget { | ||||
|                 }, | ||||
|               ).padding(horizontal: 12, vertical: 8), | ||||
|               if (data.body['title'] != null || data.body['description'] != null) | ||||
|                 _PostHeadline(data: data).padding(horizontal: 16, bottom: 8), | ||||
|               _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), | ||||
|                 _PostHeadline( | ||||
|                   data: data, | ||||
|                   isEnlarge: data.type == 'article' && showFullPost, | ||||
|                 ).padding(horizontal: 16, bottom: 8), | ||||
|               _PostContentBody( | ||||
|                 data: data, | ||||
|                 isEnlarge: data.type == 'article' && showFullPost, | ||||
|               ).padding(horizontal: 16, bottom: 6), | ||||
|               if (data.repostTo != null) | ||||
|                 _PostQuoteContent(child: data.repostTo!).padding( | ||||
|                   horizontal: 12, | ||||
| @@ -156,7 +163,7 @@ class PostItem extends StatelessWidget { | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         if (data.preload?.attachments?.isNotEmpty ?? false) | ||||
|         if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article') | ||||
|           AttachmentList( | ||||
|             data: data.preload!.attachments!, | ||||
|             bordered: true, | ||||
| @@ -292,11 +299,82 @@ class _PostBottomAction extends StatelessWidget { | ||||
|  | ||||
| class _PostHeadline extends StatelessWidget { | ||||
|   final SnPost data; | ||||
|   final bool isEnlarge; | ||||
|  | ||||
|   const _PostHeadline({super.key, required this.data}); | ||||
|   const _PostHeadline({ | ||||
|     super.key, | ||||
|     required this.data, | ||||
|     this.isEnlarge = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (isEnlarge) { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final textScaler = TextScaler.linear(1.5); | ||||
|  | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           if (data.preload?.thumbnail != null) | ||||
|             Container( | ||||
|               margin: const EdgeInsets.only(bottom: 8), | ||||
|               decoration: BoxDecoration( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                 border: Border.all( | ||||
|                   color: Theme.of(context).dividerColor, | ||||
|                   width: 1, | ||||
|                 ), | ||||
|               ), | ||||
|               child: AspectRatio( | ||||
|                 aspectRatio: 16 / 9, | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: AutoResizeUniversalImage( | ||||
|                     sn.getAttachmentUrl(data.preload!.thumbnail!.rid), | ||||
|                     fit: BoxFit.cover, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           if (data.body['title'] != null) | ||||
|             Text( | ||||
|               data.body['title'], | ||||
|               style: Theme.of(context).textTheme.titleMedium, | ||||
|               textScaler: TextScaler.linear(1.4), | ||||
|             ), | ||||
|           if (data.body['description'] != null) | ||||
|             Text( | ||||
|               data.body['description'], | ||||
|               style: Theme.of(context).textTheme.bodyMedium, | ||||
|               textScaler: TextScaler.linear(1.1), | ||||
|             ), | ||||
|           if (data.body['description'] != null) const Gap(8) else const Gap(4), | ||||
|           Row( | ||||
|             children: [ | ||||
|               Text( | ||||
|                 'articleWrittenAt'.tr( | ||||
|                   args: [DateFormat('y/M/d HH:mm').format(data.createdAt)], | ||||
|                 ), | ||||
|                 style: TextStyle(fontSize: 13), | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               if (data.updatedAt != data.createdAt) | ||||
|                 Text( | ||||
|                   'articleEditedAt'.tr( | ||||
|                     args: [DateFormat('y/M/d HH:mm').format(data.updatedAt)], | ||||
|                   ), | ||||
|                   style: TextStyle(fontSize: 13), | ||||
|                 ), | ||||
|             ], | ||||
|           ).opacity(0.75), | ||||
|           const Gap(8), | ||||
|           const Divider(height: 1, thickness: 1), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
| @@ -502,14 +580,22 @@ class _PostContentHeader extends StatelessWidget { | ||||
| } | ||||
|  | ||||
| class _PostContentBody extends StatelessWidget { | ||||
|   final dynamic data; | ||||
|   final SnPost data; | ||||
|   final bool isEnlarge; | ||||
|  | ||||
|   const _PostContentBody({this.data}); | ||||
|   const _PostContentBody({ | ||||
|     required this.data, | ||||
|     this.isEnlarge = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (data['content'] == null) return const SizedBox.shrink(); | ||||
|     return MarkdownTextContent(content: data['content']); | ||||
|     if (data.body['content'] == null) return const SizedBox.shrink(); | ||||
|     return MarkdownTextContent( | ||||
|       textScaler: isEnlarge ? TextScaler.linear(1.1) : null, | ||||
|       content: data.body['content'], | ||||
|       attachments: data.preload?.attachments, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -537,7 +623,7 @@ class _PostQuoteContent extends StatelessWidget { | ||||
|             showMenu: false, | ||||
|             onDeleted: () {}, | ||||
|           ).padding(bottom: 4), | ||||
|           _PostContentBody(data: child.body), | ||||
|           _PostContentBody(data: child), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -208,7 +208,11 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           if (thumbnail != null) const VerticalDivider(width: 1).padding(horizontal: 8), | ||||
|           if (thumbnail != null) | ||||
|             const VerticalDivider(width: 1, thickness: 1).padding( | ||||
|               horizontal: 12, | ||||
|               vertical: 16, | ||||
|             ), | ||||
|           Expanded( | ||||
|             child: ListView.separated( | ||||
|               scrollDirection: Axis.horizontal, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user