From 7c4427e84a119940842dd79e37e83eedf040bf2f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 1 May 2024 19:39:48 +0800 Subject: [PATCH] :lipstick: Optimize UX --- lib/i18n/app_zh.arb | 2 +- lib/screens/account.dart | 2 - lib/screens/auth/signin.dart | 17 +++----- lib/screens/chat/chat.dart | 52 ++++++++++-------------- lib/widgets/chat/content.dart | 1 + lib/widgets/chat/message_editor.dart | 37 ++++++----------- lib/widgets/posts/attachment_screen.dart | 4 +- 7 files changed, 46 insertions(+), 69 deletions(-) diff --git a/lib/i18n/app_zh.arb b/lib/i18n/app_zh.arb index a187af6..3a41a38 100644 --- a/lib/i18n/app_zh.arb +++ b/lib/i18n/app_zh.arb @@ -1,5 +1,5 @@ { - "appName": "Solar", + "appName": "Solar Network", "explore": "探索", "chat": "聊天", "account": "账号", diff --git a/lib/screens/account.dart b/lib/screens/account.dart index d4d52ca..0670f26 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/router.dart'; -import 'package:solian/utils/service_url.dart'; import 'package:solian/widgets/account/avatar.dart'; import 'package:solian/widgets/common_wrapper.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AccountScreen extends StatefulWidget { diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart index 256d0a3..86736b4 100644 --- a/lib/screens/auth/signin.dart +++ b/lib/screens/auth/signin.dart @@ -24,11 +24,10 @@ class SignInScreen extends StatelessWidget { router.pop(true); }).catchError((e) { List messages = e.toString().split('\n'); - if (messages.last.contains("risk")) { - final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last); + if (messages.last.contains('risk')) { + final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last); if (ticketId == null) { - context.showErrorDialog( - "requested to multi-factor authenticate, but the ticket id was not found"); + context.showErrorDialog('requested to multi-factor authenticate, but the ticket id was not found'); } showDialog( context: context, @@ -41,9 +40,7 @@ class SignInScreen extends StatelessWidget { child: Text(AppLocalizations.of(context)!.next), onPressed: () { launchUrlString( - getRequestUri( - 'passport', '/mfa?ticket=${ticketId!.group(1)}') - .toString(), + getRequestUri('passport', '/mfa?ticket=${ticketId!.group(1)}').toString(), ); if (Navigator.canPop(context)) { Navigator.pop(context); @@ -88,8 +85,7 @@ class SignInScreen extends StatelessWidget { border: const OutlineInputBorder(), labelText: AppLocalizations.of(context)!.username, ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const SizedBox(height: 12), TextField( @@ -103,8 +99,7 @@ class SignInScreen extends StatelessWidget { border: const OutlineInputBorder(), labelText: AppLocalizations.of(context)!.password, ), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onSubmitted: (_) => performSignIn(context), ), const SizedBox(height: 16), diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 1665cf9..41936f9 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -34,8 +34,7 @@ class _ChatScreenState extends State { Call? _ongoingCall; Channel? _channelMeta; - final PagingController _pagingController = - PagingController(firstPageKey: 0); + final PagingController _pagingController = PagingController(firstPageKey: 0); final http.Client _client = http.Client(); @@ -54,8 +53,7 @@ class _ChatScreenState extends State { } Future fetchCall() async { - var uri = getRequestUri( - 'messaging', '/api/channels/${widget.alias}/calls/ongoing'); + var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing'); var res = await _client.get(uri); if (res.statusCode == 200) { final result = jsonDecode(utf8.decode(res.bodyBytes)); @@ -84,10 +82,8 @@ class _ChatScreenState extends State { var res = await auth.client!.get(uri); if (res.statusCode == 200) { - final result = - PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); - final items = - result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); + final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty(); final isLastPage = (result.count - pageKey) < take; if (isLastPage || result.data == null) { _pagingController.appendLastPage(items); @@ -115,16 +111,13 @@ class _ChatScreenState extends State { void updateMessage(Message item) { setState(() { - _pagingController.itemList = _pagingController.itemList - ?.map((x) => x.id == item.id ? item : x) - .toList(); + _pagingController.itemList = _pagingController.itemList?.map((x) => x.id == item.id ? item : x).toList(); }); } void deleteMessage(Message item) { setState(() { - _pagingController.itemList = - _pagingController.itemList?.where((x) => x.id != item.id).toList(); + _pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList(); }); } @@ -154,8 +147,7 @@ class _ChatScreenState extends State { fetchCall(); }); - _pagingController - .addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); + _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); super.initState(); } @@ -165,12 +157,10 @@ class _ChatScreenState extends State { Widget chatHistoryBuilder(context, item, index) { bool isMerged = false, hasMerged = false; if (index > 0) { - hasMerged = - getMessageMergeable(_pagingController.itemList?[index - 1], item); + hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); } if (index + 1 < (_pagingController.itemList?.length ?? 0)) { - isMerged = - getMessageMergeable(item, _pagingController.itemList?[index + 1]); + isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); } return InkWell( child: Container( @@ -193,8 +183,7 @@ class _ChatScreenState extends State { final callBanner = MaterialBanner( padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), leading: const Icon(Icons.call_received), - backgroundColor: - Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), + backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9), dividerColor: const Color.fromARGB(1, 0, 0, 0), content: Text(AppLocalizations.of(context)!.chatCallOngoing), actions: [ @@ -213,15 +202,18 @@ class _ChatScreenState extends State { return IndentWrapper( hideDrawer: true, - title: _channelMeta?.name ?? "Loading...", + title: _channelMeta?.name ?? 'Loading...', appBarActions: _channelMeta != null ? [ ChannelCallAction( - call: _ongoingCall, - channel: _channelMeta!, - onUpdate: () => fetchMetadata()), + call: _ongoingCall, + channel: _channelMeta!, + onUpdate: () => fetchMetadata(), + ), ChannelManageAction( - channel: _channelMeta!, onUpdate: () => fetchMetadata()), + channel: _channelMeta!, + onUpdate: () => fetchMetadata(), + ), ] : [], child: FutureBuilder( @@ -242,8 +234,10 @@ class _ChatScreenState extends State { reverse: true, pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (_) => Container(), + animateTransitions: true, + transitionDuration: 500.ms, itemBuilder: chatHistoryBuilder, + noItemsFoundIndicatorBuilder: (_) => Container(), ), ), ), @@ -258,9 +252,7 @@ class _ChatScreenState extends State { ), ], ), - _ongoingCall != null - ? callBanner.animate().slideY() - : Container(), + _ongoingCall != null ? callBanner.animate().slideY() : Container(), ], ), onInsertMessage: (message) => addMessage(message), diff --git a/lib/widgets/chat/content.dart b/lib/widgets/chat/content.dart index 0a4b076..c78d380 100644 --- a/lib/widgets/chat/content.dart +++ b/lib/widgets/chat/content.dart @@ -13,6 +13,7 @@ class ChatMessageContent extends StatelessWidget { return Markdown( data: item.content, shrinkWrap: true, + selectable: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(0), onTapLink: (text, href, title) async { diff --git a/lib/widgets/chat/message_editor.dart b/lib/widgets/chat/message_editor.dart index 881618e..2939313 100644 --- a/lib/widgets/chat/message_editor.dart +++ b/lib/widgets/chat/message_editor.dart @@ -18,12 +18,7 @@ class ChatMessageEditor extends StatefulWidget { final Message? replying; final Function? onReset; - const ChatMessageEditor( - {super.key, - required this.channel, - this.editing, - this.replying, - this.onReset}); + const ChatMessageEditor({super.key, required this.channel, this.editing, this.replying, this.onReset}); @override State createState() => _ChatMessageEditorState(); @@ -31,6 +26,7 @@ class ChatMessageEditor extends StatefulWidget { class _ChatMessageEditorState extends State { final _textController = TextEditingController(); + final _focusNode = FocusNode(); bool _isSubmitting = false; int? _prevEditingId; @@ -51,13 +47,14 @@ class _ChatMessageEditorState extends State { Future sendMessage(BuildContext context) async { if (_isSubmitting) return; + _focusNode.requestFocus(); + final auth = context.read(); if (!await auth.isAuthorized()) return; final uri = widget.editing == null ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') - : getRequestUri('messaging', - '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); + : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); final req = Request(widget.editing == null ? "POST" : "PUT", uri); req.headers['Content-Type'] = 'application/json'; @@ -90,8 +87,7 @@ class _ChatMessageEditorState extends State { setState(() { _prevEditingId = widget.editing!.id; _textController.text = widget.editing!.content; - _attachments = - widget.editing!.attachments ?? List.empty(growable: true); + _attachments = widget.editing!.attachments ?? List.empty(growable: true); }); } } @@ -154,38 +150,31 @@ class _ChatMessageEditorState extends State { children: [ badge.Badge( showBadge: _attachments.isNotEmpty, - badgeContent: Text(_attachments.length.toString(), - style: const TextStyle(color: Colors.white)), + badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)), position: badge.BadgePosition.custom(top: -2, end: 8), child: TextButton( - style: TextButton.styleFrom( - shape: const CircleBorder(), - padding: const EdgeInsets.all(4)), - onPressed: - !_isSubmitting ? () => viewAttachments(context) : null, + style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), + onPressed: !_isSubmitting ? () => viewAttachments(context) : null, child: const Icon(Icons.attach_file), ), ), Expanded( child: TextField( + focusNode: _focusNode, controller: _textController, maxLines: null, autofocus: true, autocorrect: true, keyboardType: TextInputType.text, decoration: InputDecoration.collapsed( - hintText: - AppLocalizations.of(context)!.chatMessagePlaceholder, + hintText: AppLocalizations.of(context)!.chatMessagePlaceholder, ), onSubmitted: (_) => sendMessage(context), - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ), TextButton( - style: TextButton.styleFrom( - shape: const CircleBorder(), - padding: const EdgeInsets.all(4)), + style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)), onPressed: !_isSubmitting ? () => sendMessage(context) : null, child: const Icon(Icons.send), ) diff --git a/lib/widgets/posts/attachment_screen.dart b/lib/widgets/posts/attachment_screen.dart index 0a43c6c..8d84d3d 100755 --- a/lib/widgets/posts/attachment_screen.dart +++ b/lib/widgets/posts/attachment_screen.dart @@ -14,7 +14,9 @@ class AttachmentScreen extends StatelessWidget { child: InteractiveViewer( boundaryMargin: const EdgeInsets.all(128), minScale: 0.1, - maxScale: 16.0, + maxScale: 16, + panEnabled: true, + scaleEnabled: true, child: Image.network(url, fit: BoxFit.contain), ), );