💄 Optimize UX

This commit is contained in:
LittleSheep 2024-05-01 19:39:48 +08:00
parent fd200105c0
commit 7c4427e84a
7 changed files with 46 additions and 69 deletions

View File

@ -1,5 +1,5 @@
{ {
"appName": "Solar", "appName": "Solar Network",
"explore": "探索", "explore": "探索",
"chat": "聊天", "chat": "聊天",
"account": "账号", "account": "账号",

View File

@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/account/avatar.dart'; import 'package:solian/widgets/account/avatar.dart';
import 'package:solian/widgets/common_wrapper.dart'; import 'package:solian/widgets/common_wrapper.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AccountScreen extends StatefulWidget { class AccountScreen extends StatefulWidget {

View File

@ -24,11 +24,10 @@ class SignInScreen extends StatelessWidget {
router.pop(true); router.pop(true);
}).catchError((e) { }).catchError((e) {
List<String> messages = e.toString().split('\n'); List<String> messages = e.toString().split('\n');
if (messages.last.contains("risk")) { if (messages.last.contains('risk')) {
final ticketId = RegExp(r"ticketId=(\d+)").firstMatch(messages.last); final ticketId = RegExp(r'ticketId=(\d+)').firstMatch(messages.last);
if (ticketId == null) { if (ticketId == null) {
context.showErrorDialog( context.showErrorDialog('requested to multi-factor authenticate, but the ticket id was not found');
"requested to multi-factor authenticate, but the ticket id was not found");
} }
showDialog( showDialog(
context: context, context: context,
@ -41,9 +40,7 @@ class SignInScreen extends StatelessWidget {
child: Text(AppLocalizations.of(context)!.next), child: Text(AppLocalizations.of(context)!.next),
onPressed: () { onPressed: () {
launchUrlString( launchUrlString(
getRequestUri( getRequestUri('passport', '/mfa?ticket=${ticketId!.group(1)}').toString(),
'passport', '/mfa?ticket=${ticketId!.group(1)}')
.toString(),
); );
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
Navigator.pop(context); Navigator.pop(context);
@ -88,8 +85,7 @@ class SignInScreen extends StatelessWidget {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.username, labelText: AppLocalizations.of(context)!.username,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
TextField( TextField(
@ -103,8 +99,7 @@ class SignInScreen extends StatelessWidget {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context)!.password, labelText: AppLocalizations.of(context)!.password,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performSignIn(context), onSubmitted: (_) => performSignIn(context),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@ -34,8 +34,7 @@ class _ChatScreenState extends State<ChatScreen> {
Call? _ongoingCall; Call? _ongoingCall;
Channel? _channelMeta; Channel? _channelMeta;
final PagingController<int, Message> _pagingController = final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
PagingController(firstPageKey: 0);
final http.Client _client = http.Client(); final http.Client _client = http.Client();
@ -54,8 +53,7 @@ class _ChatScreenState extends State<ChatScreen> {
} }
Future<Call?> fetchCall() async { Future<Call?> fetchCall() async {
var uri = getRequestUri( var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing');
'messaging', '/api/channels/${widget.alias}/calls/ongoing');
var res = await _client.get(uri); var res = await _client.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes)); final result = jsonDecode(utf8.decode(res.bodyBytes));
@ -84,10 +82,8 @@ class _ChatScreenState extends State<ChatScreen> {
var res = await auth.client!.get(uri); var res = await auth.client!.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); final items = result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty();
final items =
result.data?.map((x) => Message.fromJson(x)).toList() ?? List.empty();
final isLastPage = (result.count - pageKey) < take; final isLastPage = (result.count - pageKey) < take;
if (isLastPage || result.data == null) { if (isLastPage || result.data == null) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);
@ -115,16 +111,13 @@ class _ChatScreenState extends State<ChatScreen> {
void updateMessage(Message item) { void updateMessage(Message item) {
setState(() { setState(() {
_pagingController.itemList = _pagingController.itemList _pagingController.itemList = _pagingController.itemList?.map((x) => x.id == item.id ? item : x).toList();
?.map((x) => x.id == item.id ? item : x)
.toList();
}); });
} }
void deleteMessage(Message item) { void deleteMessage(Message item) {
setState(() { setState(() {
_pagingController.itemList = _pagingController.itemList = _pagingController.itemList?.where((x) => x.id != item.id).toList();
_pagingController.itemList?.where((x) => x.id != item.id).toList();
}); });
} }
@ -154,8 +147,7 @@ class _ChatScreenState extends State<ChatScreen> {
fetchCall(); fetchCall();
}); });
_pagingController _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
super.initState(); super.initState();
} }
@ -165,12 +157,10 @@ class _ChatScreenState extends State<ChatScreen> {
Widget chatHistoryBuilder(context, item, index) { Widget chatHistoryBuilder(context, item, index) {
bool isMerged = false, hasMerged = false; bool isMerged = false, hasMerged = false;
if (index > 0) { if (index > 0) {
hasMerged = hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item);
getMessageMergeable(_pagingController.itemList?[index - 1], item);
} }
if (index + 1 < (_pagingController.itemList?.length ?? 0)) { if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
isMerged = isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]);
getMessageMergeable(item, _pagingController.itemList?[index + 1]);
} }
return InkWell( return InkWell(
child: Container( child: Container(
@ -193,8 +183,7 @@ class _ChatScreenState extends State<ChatScreen> {
final callBanner = MaterialBanner( final callBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.call_received), leading: const Icon(Icons.call_received),
backgroundColor: backgroundColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.9),
dividerColor: const Color.fromARGB(1, 0, 0, 0), dividerColor: const Color.fromARGB(1, 0, 0, 0),
content: Text(AppLocalizations.of(context)!.chatCallOngoing), content: Text(AppLocalizations.of(context)!.chatCallOngoing),
actions: [ actions: [
@ -213,15 +202,18 @@ class _ChatScreenState extends State<ChatScreen> {
return IndentWrapper( return IndentWrapper(
hideDrawer: true, hideDrawer: true,
title: _channelMeta?.name ?? "Loading...", title: _channelMeta?.name ?? 'Loading...',
appBarActions: _channelMeta != null appBarActions: _channelMeta != null
? [ ? [
ChannelCallAction( ChannelCallAction(
call: _ongoingCall, call: _ongoingCall,
channel: _channelMeta!, channel: _channelMeta!,
onUpdate: () => fetchMetadata()), onUpdate: () => fetchMetadata(),
),
ChannelManageAction( ChannelManageAction(
channel: _channelMeta!, onUpdate: () => fetchMetadata()), channel: _channelMeta!,
onUpdate: () => fetchMetadata(),
),
] ]
: [], : [],
child: FutureBuilder( child: FutureBuilder(
@ -242,8 +234,10 @@ class _ChatScreenState extends State<ChatScreen> {
reverse: true, reverse: true,
pagingController: _pagingController, pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>( builderDelegate: PagedChildBuilderDelegate<Message>(
noItemsFoundIndicatorBuilder: (_) => Container(), animateTransitions: true,
transitionDuration: 500.ms,
itemBuilder: chatHistoryBuilder, itemBuilder: chatHistoryBuilder,
noItemsFoundIndicatorBuilder: (_) => Container(),
), ),
), ),
), ),
@ -258,9 +252,7 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
], ],
), ),
_ongoingCall != null _ongoingCall != null ? callBanner.animate().slideY() : Container(),
? callBanner.animate().slideY()
: Container(),
], ],
), ),
onInsertMessage: (message) => addMessage(message), onInsertMessage: (message) => addMessage(message),

View File

@ -13,6 +13,7 @@ class ChatMessageContent extends StatelessWidget {
return Markdown( return Markdown(
data: item.content, data: item.content,
shrinkWrap: true, shrinkWrap: true,
selectable: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {

View File

@ -18,12 +18,7 @@ class ChatMessageEditor extends StatefulWidget {
final Message? replying; final Message? replying;
final Function? onReset; final Function? onReset;
const ChatMessageEditor( const ChatMessageEditor({super.key, required this.channel, this.editing, this.replying, this.onReset});
{super.key,
required this.channel,
this.editing,
this.replying,
this.onReset});
@override @override
State<ChatMessageEditor> createState() => _ChatMessageEditorState(); State<ChatMessageEditor> createState() => _ChatMessageEditorState();
@ -31,6 +26,7 @@ class ChatMessageEditor extends StatefulWidget {
class _ChatMessageEditorState extends State<ChatMessageEditor> { class _ChatMessageEditorState extends State<ChatMessageEditor> {
final _textController = TextEditingController(); final _textController = TextEditingController();
final _focusNode = FocusNode();
bool _isSubmitting = false; bool _isSubmitting = false;
int? _prevEditingId; int? _prevEditingId;
@ -51,13 +47,14 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
Future<void> sendMessage(BuildContext context) async { Future<void> sendMessage(BuildContext context) async {
if (_isSubmitting) return; if (_isSubmitting) return;
_focusNode.requestFocus();
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) return; if (!await auth.isAuthorized()) return;
final uri = widget.editing == null final uri = widget.editing == null
? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
: getRequestUri('messaging', : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}');
'/api/channels/${widget.channel}/messages/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? "POST" : "PUT", uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
@ -90,8 +87,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
setState(() { setState(() {
_prevEditingId = widget.editing!.id; _prevEditingId = widget.editing!.id;
_textController.text = widget.editing!.content; _textController.text = widget.editing!.content;
_attachments = _attachments = widget.editing!.attachments ?? List.empty(growable: true);
widget.editing!.attachments ?? List.empty(growable: true);
}); });
} }
} }
@ -154,38 +150,31 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
children: [ children: [
badge.Badge( badge.Badge(
showBadge: _attachments.isNotEmpty, showBadge: _attachments.isNotEmpty,
badgeContent: Text(_attachments.length.toString(), badgeContent: Text(_attachments.length.toString(), style: const TextStyle(color: Colors.white)),
style: const TextStyle(color: Colors.white)),
position: badge.BadgePosition.custom(top: -2, end: 8), position: badge.BadgePosition.custom(top: -2, end: 8),
child: TextButton( child: TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
shape: const CircleBorder(), onPressed: !_isSubmitting ? () => viewAttachments(context) : null,
padding: const EdgeInsets.all(4)),
onPressed:
!_isSubmitting ? () => viewAttachments(context) : null,
child: const Icon(Icons.attach_file), child: const Icon(Icons.attach_file),
), ),
), ),
Expanded( Expanded(
child: TextField( child: TextField(
focusNode: _focusNode,
controller: _textController, controller: _textController,
maxLines: null, maxLines: null,
autofocus: true, autofocus: true,
autocorrect: true, autocorrect: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
AppLocalizations.of(context)!.chatMessagePlaceholder,
), ),
onSubmitted: (_) => sendMessage(context), onSubmitted: (_) => sendMessage(context),
onTapOutside: (_) => onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(shape: const CircleBorder(), padding: const EdgeInsets.all(4)),
shape: const CircleBorder(),
padding: const EdgeInsets.all(4)),
onPressed: !_isSubmitting ? () => sendMessage(context) : null, onPressed: !_isSubmitting ? () => sendMessage(context) : null,
child: const Icon(Icons.send), child: const Icon(Icons.send),
) )

View File

@ -14,7 +14,9 @@ class AttachmentScreen extends StatelessWidget {
child: InteractiveViewer( child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(128), boundaryMargin: const EdgeInsets.all(128),
minScale: 0.1, minScale: 0.1,
maxScale: 16.0, maxScale: 16,
panEnabled: true,
scaleEnabled: true,
child: Image.network(url, fit: BoxFit.contain), child: Image.network(url, fit: BoxFit.contain),
), ),
); );