💄 Optimize UX
This commit is contained in:
parent
fd200105c0
commit
7c4427e84a
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"appName": "Solar",
|
"appName": "Solar Network",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"chat": "聊天",
|
"chat": "聊天",
|
||||||
"account": "账号",
|
"account": "账号",
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
)
|
)
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user