💄 Improvements and optimize UX

This commit is contained in:
LittleSheep 2024-09-23 22:43:13 +08:00
parent 2d347e0d41
commit 724bd6592e
10 changed files with 193 additions and 153 deletions

View File

@ -22,9 +22,9 @@
"explore": "Explore", "explore": "Explore",
"posts": "Posts", "posts": "Posts",
"unlink": "Unlink", "unlink": "Unlink",
"feedSearch": "Search Feed", "postSearch": "Search Post",
"feedSearchWithTag": "Searching with tag #@key", "postSearchWithTag": "Searching with tag #@key",
"feedSearchWithCategory": "Searching in category @category", "postSearchWithCategory": "Searching in category @category",
"feedUnreadCount": "@count posts you may missed", "feedUnreadCount": "@count posts you may missed",
"messages": "Messages", "messages": "Messages",
"messagesUnreadCount": "@count messages unread", "messagesUnreadCount": "@count messages unread",

View File

@ -32,9 +32,9 @@
"dashboard": "仪表盘", "dashboard": "仪表盘",
"today": "今日", "today": "今日",
"yesterday": "昨日", "yesterday": "昨日",
"feedSearch": "搜索资讯", "postSearch": "搜索帖子",
"feedSearchWithTag": "检索带有 #@key 标签的资讯", "postSearchWithTag": "检索带有 #@key 标签的资讯",
"feedSearchWithCategory": "检索位于分类 @category 的资讯", "postSearchWithCategory": "检索位于分类 @category 的资讯",
"feedUnreadCount": "@count 条你可能错过的帖子", "feedUnreadCount": "@count 条你可能错过的帖子",
"messages": "消息", "messages": "消息",
"messagesUnreadCount": "@count 条未读的消息", "messagesUnreadCount": "@count 条未读的消息",

View File

@ -23,7 +23,7 @@ import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.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/posts/post_editor.dart';
import 'package:solian/screens/settings.dart'; import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
@ -78,13 +78,18 @@ abstract class AppRouter {
builder: (context, state, child) => child, builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/feed', path: '/explore',
name: 'feed', name: 'explore',
builder: (context, state) => const FeedScreen(), builder: (context, state) => const ExploreScreen(),
), ),
GoRoute( GoRoute(
path: '/feed/search', path: '/drafts',
name: 'feedSearch', name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute(
path: '/posts/search',
name: 'postSearch',
builder: (context, state) => TitleShell( builder: (context, state) => TitleShell(
state: state, state: state,
child: FeedSearchScreen( child: FeedSearchScreen(
@ -93,11 +98,6 @@ abstract class AppRouter {
), ),
), ),
), ),
GoRoute(
path: '/drafts',
name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute( GoRoute(
path: '/posts/view/:id', path: '/posts/view/:id',
name: 'postDetail', name: 'postDetail',

View File

@ -54,6 +54,7 @@ class AboutScreen extends StatelessWidget {
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
alignment: WrapAlignment.center,
children: [ children: [
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,

View File

@ -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_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/posts/post_warped_list.dart';
class FeedScreen extends StatefulWidget { class ExploreScreen extends StatefulWidget {
const FeedScreen({super.key}); const ExploreScreen({super.key});
@override @override
State<FeedScreen> createState() => _FeedScreenState(); State<ExploreScreen> createState() => _ExploreScreenState();
} }
class _FeedScreenState extends State<FeedScreen> class _ExploreScreenState extends State<ExploreScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final PostListController _postController; late final PostListController _postController;
late final TabController _tabController; late final TabController _tabController;
@ -82,7 +82,7 @@ class _FeedScreenState extends State<FeedScreen>
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [ return [
SliverAppBar( SliverAppBar(
title: AppBarTitle('feed'.tr), title: AppBarTitle('explore'.tr),
centerTitle: false, centerTitle: false,
floating: true, floating: true,
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),

View File

@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
ListTile( ListTile(
leading: const Icon(Icons.label), leading: const Icon(Icons.label),
tileColor: Theme.of(context).colorScheme.surfaceContainer, tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})), title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
), ),
if (widget.category != null) if (widget.category != null)
ListTile( ListTile(
leading: const Icon(Icons.category), leading: const Icon(Icons.category),
tileColor: Theme.of(context).colorScheme.surfaceContainer, tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithCategory' title: Text('postSearchWithCategory'
.trParams({'key': widget.category!})), .trParams({'key': widget.category!})),
), ),
Expanded( Expanded(

View File

@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget {
return MarkdownTextContent( return MarkdownTextContent(
parentId: 'm${item.id}', parentId: 'm${item.id}',
isSelectable: true, isSelectable: true,
isAutoWarp: true,
content: body.text, content: body.text,
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart'; import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown/markdown.dart'; import 'package:markdown/markdown.dart';
@ -15,6 +16,7 @@ class MarkdownTextContent extends StatelessWidget {
final String parentId; final String parentId;
final bool isSelectable; final bool isSelectable;
final bool isLargeText; final bool isLargeText;
final bool isAutoWarp;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
@ -22,139 +24,175 @@ class MarkdownTextContent extends StatelessWidget {
required this.parentId, required this.parentId,
this.isSelectable = false, this.isSelectable = false,
this.isLargeText = false, this.isLargeText = false,
this.isAutoWarp = false,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final emojiRegex = RegExp(r':([-\w]+):'); final stickerRegex = RegExp(r':([-\w]+):');
final emojiMatch = emojiRegex.allMatches(content);
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
return Markdown( // Split the content into paragraphs
shrinkWrap: true, final paragraphs = content.split(RegExp(r'\n\s*\n'));
physics: const NeverScrollableScrollPhysics(),
data: content, // Iterate over each paragraph to process stickers individually
padding: EdgeInsets.zero, List<Widget> contentWidgets = [];
styleSheet: MarkdownStyleSheet.fromTheme( for (var idx = 0; idx < paragraphs.length; idx++) {
Theme.of(context), // Getting paragraph
).copyWith( var paragraph = paragraphs[idx];
textScaleFactor: isLargeText ? 1.1 : 1,
blockquote: TextStyle( // Auto adding new-lines
color: Theme.of(context).colorScheme.onSurfaceVariant, if (isAutoWarp) {
), paragraph = paragraph.replaceAll('\n', '\\\n');
blockquoteDecoration: BoxDecoration( }
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)), // Matching stickers
), final stickerMatch = stickerRegex.allMatches(paragraph);
horizontalRuleDecoration: BoxDecoration( final isOnlySticker =
border: Border( paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
top: BorderSide(
width: 1.0, contentWidgets.add(
color: Theme.of(context).dividerColor, 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,
extensionSet: markdown.ExtensionSet( <markdown.InlineSyntax>[
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, _UserNameCardInlineSyntax(),
<markdown.InlineSyntax>[ _CustomEmoteInlineSyntax(),
_UserNameCardInlineSyntax(), markdown.EmojiSyntax(),
_CustomEmoteInlineSyntax(), markdown.AutolinkSyntax(),
markdown.EmojiSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.AutolinkSyntax(), ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
markdown.AutolinkExtensionSyntax(), ],
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes ),
], onTapLink: (text, href, title) async {
), if (href == null) return;
onTapLink: (text, href, title) async { if (href.startsWith('solink://')) {
if (href == null) return; final segments = href.replaceFirst('solink://', '').split('/');
if (href.startsWith('solink://')) { switch (segments[0]) {
final segments = href.replaceFirst('solink://', '').split('/'); case 'users':
switch (segments[0]) { showModalBottomSheet(
case 'users': useRootNavigator: true,
showModalBottomSheet( isScrollControlled: true,
useRootNavigator: true, backgroundColor: Theme.of(context).colorScheme.surface,
isScrollControlled: true, context: context,
backgroundColor: Theme.of(context).colorScheme.surface, builder: (context) => AccountProfilePopup(
context: context, name: segments[1],
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;
} }
fit = BoxFit.contain; return;
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 AutoCacheImage( await launchUrlString(
url, href,
width: width, mode: LaunchMode.externalApplication,
height: height, );
fit: fit, },
); 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,
); );
} }

View File

@ -9,9 +9,9 @@ abstract class AppNavigation {
page: 'dashboard', page: 'dashboard',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: Icons.newspaper, icon: Icons.explore,
label: 'feed'.tr, label: 'explore'.tr,
page: 'feed', page: 'explore',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: Icons.workspaces, icon: Icons.workspaces,

View File

@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget {
), ),
), ),
onTap: () { onTap: () {
AppRouter.instance.pushNamed('feedSearch', queryParameters: { AppRouter.instance.pushNamed('postSearch', queryParameters: {
'tag': x.alias, 'tag': x.alias,
}); });
}, },