diff --git a/assets/locales/en_us.json b/assets/locales/en_us.json index 13fb80b..d53369b 100644 --- a/assets/locales/en_us.json +++ b/assets/locales/en_us.json @@ -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", diff --git a/assets/locales/zh_cn.json b/assets/locales/zh_cn.json index a2b3285..5860ede 100644 --- a/assets/locales/zh_cn.json +++ b/assets/locales/zh_cn.json @@ -32,9 +32,9 @@ "dashboard": "仪表盘", "today": "今日", "yesterday": "昨日", - "feedSearch": "搜索资讯", - "feedSearchWithTag": "检索带有 #@key 标签的资讯", - "feedSearchWithCategory": "检索位于分类 @category 的资讯", + "postSearch": "搜索帖子", + "postSearchWithTag": "检索带有 #@key 标签的资讯", + "postSearchWithCategory": "检索位于分类 @category 的资讯", "feedUnreadCount": "@count 条你可能错过的帖子", "messages": "消息", "messagesUnreadCount": "@count 条未读的消息", diff --git a/lib/router.dart b/lib/router.dart index 3bd980a..a84b0df 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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', diff --git a/lib/screens/about.dart b/lib/screens/about.dart index 739054a..5a42739 100644 --- a/lib/screens/about.dart +++ b/lib/screens/about.dart @@ -54,6 +54,7 @@ class AboutScreen extends StatelessWidget { child: Wrap( spacing: 8, runSpacing: 8, + alignment: WrapAlignment.center, children: [ TextButton( style: denseButtonStyle, diff --git a/lib/screens/feed.dart b/lib/screens/explore.dart similarity index 97% rename from lib/screens/feed.dart rename to lib/screens/explore.dart index f80bbae..586c22f 100644 --- a/lib/screens/feed.dart +++ b/lib/screens/explore.dart @@ -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 createState() => _FeedScreenState(); + State createState() => _ExploreScreenState(); } -class _FeedScreenState extends State +class _ExploreScreenState extends State with SingleTickerProviderStateMixin { late final PostListController _postController; late final TabController _tabController; @@ -82,7 +82,7 @@ class _FeedScreenState extends State headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar( - title: AppBarTitle('feed'.tr), + title: AppBarTitle('explore'.tr), centerTitle: false, floating: true, toolbarHeight: AppTheme.toolbarHeight(context), diff --git a/lib/screens/feed/search.dart b/lib/screens/feed/search.dart index 0ff0b2b..c4d74cd 100644 --- a/lib/screens/feed/search.dart +++ b/lib/screens/feed/search.dart @@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State { 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( diff --git a/lib/widgets/chat/chat_event_message.dart b/lib/widgets/chat/chat_event_message.dart index 644e62f..f933d0b 100644 --- a/lib/widgets/chat/chat_event_message.dart +++ b/lib/widgets/chat/chat_event_message.dart @@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget { return MarkdownTextContent( parentId: 'm${item.id}', isSelectable: true, + isAutoWarp: true, content: body.text, ); } diff --git a/lib/widgets/markdown_text_content.dart b/lib/widgets/markdown_text_content.dart index eb706ef..61b12d5 100644 --- a/lib/widgets/markdown_text_content.dart +++ b/lib/widgets/markdown_text_content.dart @@ -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 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, - [ - _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, + [ + _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, ); } diff --git a/lib/widgets/navigation/app_navigation.dart b/lib/widgets/navigation/app_navigation.dart index 2a66d68..f56a627 100644 --- a/lib/widgets/navigation/app_navigation.dart +++ b/lib/widgets/navigation/app_navigation.dart @@ -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, diff --git a/lib/widgets/posts/post_tags.dart b/lib/widgets/posts/post_tags.dart index 9d74545..905cc05 100644 --- a/lib/widgets/posts/post_tags.dart +++ b/lib/widgets/posts/post_tags.dart @@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget { ), ), onTap: () { - AppRouter.instance.pushNamed('feedSearch', queryParameters: { + AppRouter.instance.pushNamed('postSearch', queryParameters: { 'tag': x.alias, }); },