💄 Improvements and optimize UX
This commit is contained in:
		| @@ -22,9 +22,9 @@ | ||||
|   "explore": "Explore", | ||||
|   "posts": "Posts", | ||||
|   "unlink": "Unlink", | ||||
|   "feedSearch": "Search Feed", | ||||
|   "feedSearchWithTag": "Searching with tag #@key", | ||||
|   "feedSearchWithCategory": "Searching in category @category", | ||||
|   "postSearch": "Search Post", | ||||
|   "postSearchWithTag": "Searching with tag #@key", | ||||
|   "postSearchWithCategory": "Searching in category @category", | ||||
|   "feedUnreadCount": "@count posts you may missed", | ||||
|   "messages": "Messages", | ||||
|   "messagesUnreadCount": "@count messages unread", | ||||
|   | ||||
| @@ -32,9 +32,9 @@ | ||||
|   "dashboard": "仪表盘", | ||||
|   "today": "今日", | ||||
|   "yesterday": "昨日", | ||||
|   "feedSearch": "搜索资讯", | ||||
|   "feedSearchWithTag": "检索带有 #@key 标签的资讯", | ||||
|   "feedSearchWithCategory": "检索位于分类 @category 的资讯", | ||||
|   "postSearch": "搜索帖子", | ||||
|   "postSearchWithTag": "检索带有 #@key 标签的资讯", | ||||
|   "postSearchWithCategory": "检索位于分类 @category 的资讯", | ||||
|   "feedUnreadCount": "@count 条你可能错过的帖子", | ||||
|   "messages": "消息", | ||||
|   "messagesUnreadCount": "@count 条未读的消息", | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import 'package:solian/screens/realms.dart'; | ||||
| import 'package:solian/screens/realms/realm_detail.dart'; | ||||
| import 'package:solian/screens/realms/realm_organize.dart'; | ||||
| import 'package:solian/screens/realms/realm_view.dart'; | ||||
| import 'package:solian/screens/feed.dart'; | ||||
| import 'package:solian/screens/explore.dart'; | ||||
| import 'package:solian/screens/posts/post_editor.dart'; | ||||
| import 'package:solian/screens/settings.dart'; | ||||
| import 'package:solian/shells/root_shell.dart'; | ||||
| @@ -78,13 +78,18 @@ abstract class AppRouter { | ||||
|     builder: (context, state, child) => child, | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/feed', | ||||
|         name: 'feed', | ||||
|         builder: (context, state) => const FeedScreen(), | ||||
|         path: '/explore', | ||||
|         name: 'explore', | ||||
|         builder: (context, state) => const ExploreScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/feed/search', | ||||
|         name: 'feedSearch', | ||||
|         path: '/drafts', | ||||
|         name: 'draftBox', | ||||
|         builder: (context, state) => const DraftBoxScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/posts/search', | ||||
|         name: 'postSearch', | ||||
|         builder: (context, state) => TitleShell( | ||||
|           state: state, | ||||
|           child: FeedSearchScreen( | ||||
| @@ -93,11 +98,6 @@ abstract class AppRouter { | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/drafts', | ||||
|         name: 'draftBox', | ||||
|         builder: (context, state) => const DraftBoxScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/posts/view/:id', | ||||
|         name: 'postDetail', | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class AboutScreen extends StatelessWidget { | ||||
|               child: Wrap( | ||||
|                 spacing: 8, | ||||
|                 runSpacing: 8, | ||||
|                 alignment: WrapAlignment.center, | ||||
|                 children: [ | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|   | ||||
| @@ -16,14 +16,14 @@ import 'package:solian/widgets/app_bar_leading.dart'; | ||||
| import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; | ||||
| import 'package:solian/widgets/posts/post_warped_list.dart'; | ||||
| 
 | ||||
| class FeedScreen extends StatefulWidget { | ||||
|   const FeedScreen({super.key}); | ||||
| class ExploreScreen extends StatefulWidget { | ||||
|   const ExploreScreen({super.key}); | ||||
| 
 | ||||
|   @override | ||||
|   State<FeedScreen> createState() => _FeedScreenState(); | ||||
|   State<ExploreScreen> createState() => _ExploreScreenState(); | ||||
| } | ||||
| 
 | ||||
| class _FeedScreenState extends State<FeedScreen> | ||||
| class _ExploreScreenState extends State<ExploreScreen> | ||||
|     with SingleTickerProviderStateMixin { | ||||
|   late final PostListController _postController; | ||||
|   late final TabController _tabController; | ||||
| @@ -82,7 +82,7 @@ class _FeedScreenState extends State<FeedScreen> | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             return [ | ||||
|               SliverAppBar( | ||||
|                 title: AppBarTitle('feed'.tr), | ||||
|                 title: AppBarTitle('explore'.tr), | ||||
|                 centerTitle: false, | ||||
|                 floating: true, | ||||
|                 toolbarHeight: AppTheme.toolbarHeight(context), | ||||
| @@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> { | ||||
|               ListTile( | ||||
|                 leading: const Icon(Icons.label), | ||||
|                 tileColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})), | ||||
|                 title: Text('postSearchWithTag'.trParams({'key': widget.tag!})), | ||||
|               ), | ||||
|             if (widget.category != null) | ||||
|               ListTile( | ||||
|                 leading: const Icon(Icons.category), | ||||
|                 tileColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 title: Text('feedSearchWithCategory' | ||||
|                 title: Text('postSearchWithCategory' | ||||
|                     .trParams({'key': widget.category!})), | ||||
|               ), | ||||
|             Expanded( | ||||
|   | ||||
| @@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget { | ||||
|     return MarkdownTextContent( | ||||
|       parentId: 'm${item.id}', | ||||
|       isSelectable: true, | ||||
|       isAutoWarp: true, | ||||
|       content: body.text, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown_selectionarea/flutter_markdown.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:markdown/markdown.dart' as markdown; | ||||
| import 'package:markdown/markdown.dart'; | ||||
| @@ -15,6 +16,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|   final String parentId; | ||||
|   final bool isSelectable; | ||||
|   final bool isLargeText; | ||||
|   final bool isAutoWarp; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
|     super.key, | ||||
| @@ -22,139 +24,175 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|     required this.parentId, | ||||
|     this.isSelectable = false, | ||||
|     this.isLargeText = false, | ||||
|     this.isAutoWarp = false, | ||||
|   }); | ||||
|  | ||||
|   Widget _buildContent(BuildContext context) { | ||||
|     final emojiRegex = RegExp(r':([-\w]+):'); | ||||
|     final emojiMatch = emojiRegex.allMatches(content); | ||||
|     final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty; | ||||
|     final stickerRegex = RegExp(r':([-\w]+):'); | ||||
|  | ||||
|     return Markdown( | ||||
|       shrinkWrap: true, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
|       data: content, | ||||
|       padding: EdgeInsets.zero, | ||||
|       styleSheet: MarkdownStyleSheet.fromTheme( | ||||
|         Theme.of(context), | ||||
|       ).copyWith( | ||||
|         textScaleFactor: isLargeText ? 1.1 : 1, | ||||
|         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, | ||||
|     // Split the content into paragraphs | ||||
|     final paragraphs = content.split(RegExp(r'\n\s*\n')); | ||||
|  | ||||
|     // Iterate over each paragraph to process stickers individually | ||||
|     List<Widget> contentWidgets = []; | ||||
|     for (var idx = 0; idx < paragraphs.length; idx++) { | ||||
|       // Getting paragraph | ||||
|       var paragraph = paragraphs[idx]; | ||||
|  | ||||
|       // Auto adding new-lines | ||||
|       if (isAutoWarp) { | ||||
|         paragraph = paragraph.replaceAll('\n', '\\\n'); | ||||
|       } | ||||
|  | ||||
|       // Matching stickers | ||||
|       final stickerMatch = stickerRegex.allMatches(paragraph); | ||||
|       final isOnlySticker = | ||||
|           paragraph.replaceAll(stickerRegex, '').trim().isEmpty; | ||||
|  | ||||
|       contentWidgets.add( | ||||
|         Markdown( | ||||
|           shrinkWrap: true, | ||||
|           physics: const NeverScrollableScrollPhysics(), | ||||
|           data: paragraph, | ||||
|           padding: EdgeInsets.zero, | ||||
|           styleSheet: MarkdownStyleSheet.fromTheme( | ||||
|             Theme.of(context), | ||||
|           ).copyWith( | ||||
|             textScaleFactor: isLargeText ? 1.1 : 1, | ||||
|             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, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       extensionSet: markdown.ExtensionSet( | ||||
|         markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|         <markdown.InlineSyntax>[ | ||||
|           _UserNameCardInlineSyntax(), | ||||
|           _CustomEmoteInlineSyntax(), | ||||
|           markdown.EmojiSyntax(), | ||||
|           markdown.AutolinkSyntax(), | ||||
|           markdown.AutolinkExtensionSyntax(), | ||||
|           ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes | ||||
|         ], | ||||
|       ), | ||||
|       onTapLink: (text, href, title) async { | ||||
|         if (href == null) return; | ||||
|         if (href.startsWith('solink://')) { | ||||
|           final segments = href.replaceFirst('solink://', '').split('/'); | ||||
|           switch (segments[0]) { | ||||
|             case 'users': | ||||
|               showModalBottomSheet( | ||||
|                 useRootNavigator: true, | ||||
|                 isScrollControlled: true, | ||||
|                 backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|                 context: context, | ||||
|                 builder: (context) => AccountProfilePopup( | ||||
|                   name: segments[1], | ||||
|                 ), | ||||
|               ); | ||||
|           } | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         await launchUrlString( | ||||
|           href, | ||||
|           mode: LaunchMode.externalApplication, | ||||
|         ); | ||||
|       }, | ||||
|       imageBuilder: (uri, title, alt) { | ||||
|         var url = uri.toString(); | ||||
|         double? width, height; | ||||
|         BoxFit? fit; | ||||
|         if (url.startsWith('solink://')) { | ||||
|           final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|           switch (segments[0]) { | ||||
|             case 'stickers': | ||||
|               double radius = 8; | ||||
|               final StickerProvider sticker = Get.find(); | ||||
|               if (emojiMatch.length <= 1 && isOnlyEmoji) { | ||||
|                 width = 128; | ||||
|                 height = 128; | ||||
|               } else if (emojiMatch.length <= 3 && isOnlyEmoji) { | ||||
|                 width = 32; | ||||
|                 height = 32; | ||||
|               } else { | ||||
|                 radius = 4; | ||||
|                 width = 16; | ||||
|                 height = 16; | ||||
|           extensionSet: markdown.ExtensionSet( | ||||
|             markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|             <markdown.InlineSyntax>[ | ||||
|               _UserNameCardInlineSyntax(), | ||||
|               _CustomEmoteInlineSyntax(), | ||||
|               markdown.EmojiSyntax(), | ||||
|               markdown.AutolinkSyntax(), | ||||
|               markdown.AutolinkExtensionSyntax(), | ||||
|               ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes | ||||
|             ], | ||||
|           ), | ||||
|           onTapLink: (text, href, title) async { | ||||
|             if (href == null) return; | ||||
|             if (href.startsWith('solink://')) { | ||||
|               final segments = href.replaceFirst('solink://', '').split('/'); | ||||
|               switch (segments[0]) { | ||||
|                 case 'users': | ||||
|                   showModalBottomSheet( | ||||
|                     useRootNavigator: true, | ||||
|                     isScrollControlled: true, | ||||
|                     backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|                     context: context, | ||||
|                     builder: (context) => AccountProfilePopup( | ||||
|                       name: segments[1], | ||||
|                     ), | ||||
|                   ); | ||||
|               } | ||||
|               fit = BoxFit.contain; | ||||
|               return ClipRRect( | ||||
|                 borderRadius: BorderRadius.all(Radius.circular(radius)), | ||||
|                 child: Container( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                   child: FutureBuilder( | ||||
|                     future: sticker.getStickerByAlias(segments[1]), | ||||
|                     builder: (context, snapshot) { | ||||
|                       if (!snapshot.hasData) { | ||||
|                         return const Center(child: CircularProgressIndicator()); | ||||
|                       } | ||||
|                       return AutoCacheImage( | ||||
|                         snapshot.data!.imageUrl, | ||||
|                         width: width, | ||||
|                         height: height, | ||||
|                         fit: fit, | ||||
|                         noErrorWidget: true, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ).paddingSymmetric(vertical: 4); | ||||
|             case 'attachments': | ||||
|               const radius = BorderRadius.all(Radius.circular(8)); | ||||
|               return LimitedBox( | ||||
|                 maxHeight: MediaQuery.of(context).size.width, | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: radius, | ||||
|                   child: AttachmentSelfContainedEntry( | ||||
|                     isDense: true, | ||||
|                     parentId: parentId, | ||||
|                     rid: segments[1], | ||||
|                   ), | ||||
|                 ), | ||||
|               ).paddingSymmetric(vertical: 4); | ||||
|           } | ||||
|         } | ||||
|               return; | ||||
|             } | ||||
|  | ||||
|         return AutoCacheImage( | ||||
|           url, | ||||
|           width: width, | ||||
|           height: height, | ||||
|           fit: fit, | ||||
|         ); | ||||
|       }, | ||||
|             await launchUrlString( | ||||
|               href, | ||||
|               mode: LaunchMode.externalApplication, | ||||
|             ); | ||||
|           }, | ||||
|           imageBuilder: (uri, title, alt) { | ||||
|             var url = uri.toString(); | ||||
|             double? width, height; | ||||
|             BoxFit? fit; | ||||
|             if (url.startsWith('solink://')) { | ||||
|               final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|               switch (segments[0]) { | ||||
|                 case 'stickers': | ||||
|                   double radius = 8; | ||||
|                   final StickerProvider sticker = Get.find(); | ||||
|  | ||||
|                   // Adjust sticker size based on the sticker count in this paragraph | ||||
|                   if (stickerMatch.length <= 1 && isOnlySticker) { | ||||
|                     width = 128; | ||||
|                     height = 128; | ||||
|                   } else if (stickerMatch.length <= 3 && isOnlySticker) { | ||||
|                     width = 32; | ||||
|                     height = 32; | ||||
|                   } else { | ||||
|                     radius = 4; | ||||
|                     width = 16; | ||||
|                     height = 16; | ||||
|                   } | ||||
|                   fit = BoxFit.contain; | ||||
|                   return ClipRRect( | ||||
|                     borderRadius: BorderRadius.all(Radius.circular(radius)), | ||||
|                     child: Container( | ||||
|                       width: width, | ||||
|                       height: height, | ||||
|                       color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                       child: FutureBuilder( | ||||
|                         future: sticker.getStickerByAlias(segments[1]), | ||||
|                         builder: (context, snapshot) { | ||||
|                           if (!snapshot.hasData) { | ||||
|                             return const Center( | ||||
|                                 child: CircularProgressIndicator()); | ||||
|                           } | ||||
|                           return AutoCacheImage( | ||||
|                             snapshot.data!.imageUrl, | ||||
|                             width: width, | ||||
|                             height: height, | ||||
|                             fit: fit, | ||||
|                             noErrorWidget: true, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ).paddingSymmetric(vertical: 4); | ||||
|                 case 'attachments': | ||||
|                   const radius = BorderRadius.all(Radius.circular(8)); | ||||
|                   return LimitedBox( | ||||
|                     maxHeight: MediaQuery.of(context).size.width, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: radius, | ||||
|                       child: AttachmentSelfContainedEntry( | ||||
|                         isDense: true, | ||||
|                         parentId: parentId, | ||||
|                         rid: segments[1], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ).paddingSymmetric(vertical: 4); | ||||
|               } | ||||
|             } | ||||
|             return AutoCacheImage( | ||||
|               url, | ||||
|               width: width, | ||||
|               height: height, | ||||
|               fit: fit, | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       if (idx < paragraphs.length - 1) { | ||||
|         contentWidgets.add(const Gap(4)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Return the list of widgets for the paragraphs | ||||
|     return Column( | ||||
|       mainAxisSize: MainAxisSize.min, | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: contentWidgets, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -9,9 +9,9 @@ abstract class AppNavigation { | ||||
|       page: 'dashboard', | ||||
|     ), | ||||
|     AppNavigationDestination( | ||||
|       icon: Icons.newspaper, | ||||
|       label: 'feed'.tr, | ||||
|       page: 'feed', | ||||
|       icon: Icons.explore, | ||||
|       label: 'explore'.tr, | ||||
|       page: 'explore', | ||||
|     ), | ||||
|     AppNavigationDestination( | ||||
|       icon: Icons.workspaces, | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget { | ||||
|                 ), | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 AppRouter.instance.pushNamed('feedSearch', queryParameters: { | ||||
|                 AppRouter.instance.pushNamed('postSearch', queryParameters: { | ||||
|                   'tag': x.alias, | ||||
|                 }); | ||||
|               }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user