diff --git a/lib/grpc/grpc_repo.dart b/lib/grpc/grpc_repo.dart index e3b8f430..ada06f1b 100644 --- a/lib/grpc/grpc_repo.dart +++ b/lib/grpc/grpc_repo.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:PiliPlus/build_config.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/grpc/bilibili/metadata.pb.dart'; import 'package:PiliPlus/grpc/bilibili/metadata/device.pb.dart'; @@ -60,6 +61,7 @@ class GrpcUrl { static const keywordBlockingList = '$im2/KeywordBlockingList'; static const keywordBlockingAdd = '$im2/KeywordBlockingAdd'; static const keywordBlockingDelete = '$im2/KeywordBlockingDelete'; + static const syncFetchSessionMsgs = '$im/SyncFetchSessionMsgs'; } class GrpcRepo { @@ -205,8 +207,11 @@ class GrpcRepo { try { final grpcMsg = Status.fromBuffer(msgBytes); // UNKNOWN : -400 : msg - msg = - '${grpcMsg.code} : ${grpcMsg.message} : ${grpcMsg.details.firstOrNull?.status.message}'; + final errMsg = + grpcMsg.details.map((e) => e.status.message).join('\n'); + msg = BuildConfig.isDebug + ? 'CODE: ${grpcMsg.code}(${grpcMsg.message})\nMSG: $errMsg' + : errMsg; } catch (e) { msg = utf8 .decode(msgBytes, allowMalformed: true) diff --git a/lib/grpc/im.dart b/lib/grpc/im.dart index aadb7726..19769e44 100644 --- a/lib/grpc/im.dart +++ b/lib/grpc/im.dart @@ -41,8 +41,28 @@ class ImGrpc { ); } - static Future> sessionMain( - {PbMap? offset}) { + static Future> syncFetchSessionMsgs({ + required int talkerId, + Int64? endSeqno, + Int64? beginSeqno, + }) { + return GrpcRepo.request( + GrpcUrl.syncFetchSessionMsgs, + ReqSessionMsg( + talkerId: Int64(talkerId), + sessionType: 1, + endSeqno: endSeqno, + beginSeqno: beginSeqno, + size: 20, + devId: '1', + ), + RspSessionMsg.fromBuffer, + ); + } + + static Future> sessionMain({ + PbMap? offset, + }) { return GrpcRepo.request( GrpcUrl.sessionMain, SessionMainReq( diff --git a/lib/pages/video/reply/widgets/zan_grpc.dart b/lib/pages/video/reply/widgets/zan_grpc.dart index c3cdd1c3..f7729293 100644 --- a/lib/pages/video/reply/widgets/zan_grpc.dart +++ b/lib/pages/video/reply/widgets/zan_grpc.dart @@ -46,9 +46,9 @@ class _ZanButtonGrpcState extends State { widget.replyItem.like = $fixnum.Int64(widget.replyItem.like.toInt() - 1); } - widget.replyItem.replyControl.action = $fixnum.Int64(2); + widget.replyItem.replyControl.action = $fixnum.Int64.TWO; } else { - widget.replyItem.replyControl.action = $fixnum.Int64(0); + widget.replyItem.replyControl.action = $fixnum.Int64.ZERO; } setState(() {}); } else { @@ -76,11 +76,11 @@ class _ZanButtonGrpcState extends State { if (action == 1) { widget.replyItem.like = $fixnum.Int64(widget.replyItem.like.toInt() + 1); - widget.replyItem.replyControl.action = $fixnum.Int64(1); + widget.replyItem.replyControl.action = $fixnum.Int64.ONE; } else { widget.replyItem.like = $fixnum.Int64(widget.replyItem.like.toInt() - 1); - widget.replyItem.replyControl.action = $fixnum.Int64(0); + widget.replyItem.replyControl.action = $fixnum.Int64.ZERO; } setState(() {}); } else { diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 966f6fd6..5cfdae91 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,67 +1,70 @@ import 'dart:async'; import 'dart:convert'; +import 'package:PiliPlus/grpc/bilibili/im/interfaces/v1.pb.dart' + show EmotionInfo, RspSessionMsg; +import 'package:PiliPlus/grpc/bilibili/im/type.pb.dart' show Msg, MsgType; +import 'package:PiliPlus/grpc/im.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/msg.dart'; -import 'package:PiliPlus/models/msg/session.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -class WhisperDetailController - extends CommonListController { +class WhisperDetailController extends CommonListController { late final ownerMid = Accounts.main.mid; late int talkerId; late String name; late String face; - String? mid; + int? mid; - int? msgSeqno; + Int64? msgSeqno; //表情转换图片规则 - List? eInfos; + List? eInfos; @override void onInit() { super.onInit(); + talkerId = int.parse(Get.parameters['talkerId']!); name = Get.parameters['name']!; face = Get.parameters['face']!; - mid = Get.parameters['mid']; + mid = Get.parameters['mid'] != null + ? int.parse(Get.parameters['mid']!) + : null; queryData(); } @override - bool customHandleResponse( - bool isRefresh, Success response) { - List? messageList = response.response.messages; - if (messageList?.isNotEmpty == true) { - msgSeqno = messageList!.last.msgSeqno; - if (messageList.length == 1 && - messageList.last.msgType == 18 && - messageList.last.msgSource == 18) { + bool customHandleResponse(bool isRefresh, Success response) { + List msgs = response.response.messages; + if (msgs.isNotEmpty) { + msgSeqno = msgs.last.msgSeqno; + if (msgs.length == 1 && + msgs.last.msgType == 18 && + msgs.last.msgSource == 18) { // debugPrint(messageList.last); // debugPrint(messageList.last.content); //{content: [{"text":"对方主动回复或关注你前,最多发送1条消息","color_day":"#9499A0","color_nig":"#9499A0"}]} } else { - ackSessionMsg(messageList.last.msgSeqno); - } - if (response.response.eInfos != null) { - eInfos ??= []; - eInfos!.addAll(response.response.eInfos!); + ackSessionMsg(msgs.last.msgSeqno.toInt()); } + eInfos ??= []; + eInfos!.addAll(response.response.eInfos); } return false; } // 消息标记已读 - Future ackSessionMsg(int? msgSeqno) async { + Future ackSessionMsg(int msgSeqno) async { var res = await MsgHttp.ackSessionMsg( talkerId: talkerId, ackSeqno: msgSeqno, @@ -79,37 +82,24 @@ class WhisperDetailController int? index, }) async { feedBack(); + SmartDialog.dismiss(); if (ownerMid == 0) { - SmartDialog.dismiss(); SmartDialog.showToast('请先登录'); return; } - if (mid == null) { - SmartDialog.dismiss(); - SmartDialog.showToast('这里不能发'); - return; - } - if (picMsg == null && message == '') { - SmartDialog.dismiss(); - SmartDialog.showToast('请输入内容'); - return; - } - var result = await MsgHttp.sendMsg( + var result = await ImGrpc.sendMsg( senderUid: ownerMid, - receiverId: int.parse(mid!), - content: msgType == 5 - ? message - : jsonEncode( - picMsg ?? {"content": message}, - ), - msgType: msgType ?? (picMsg != null ? 2 : 1), + receiverId: mid!, + content: + msgType == 5 ? message : jsonEncode(picMsg ?? {"content": message}), + msgType: MsgType.values[msgType ?? (picMsg != null ? 2 : 1)], ); SmartDialog.dismiss(); - if (result['status']) { + if (result.isSuccess) { if (msgType == 5) { - List list = (loadingState.value as Success).response; - list[index!].msgStatus = 1; - loadingState.refresh(); + loadingState + ..value.data![index!].msgStatus = 1 + ..refresh(); SmartDialog.showToast('撤回成功'); } else { onRefresh(); @@ -117,12 +107,12 @@ class WhisperDetailController SmartDialog.showToast('发送成功'); } } else { - SmartDialog.showToast(result['msg']); + result.toast(); } } @override - List? getDataList(SessionMsgDataModel response) { + List? getDataList(RspSessionMsg response) { if (response.hasMore == 0) { isEnd = true; } @@ -138,10 +128,10 @@ class WhisperDetailController } @override - Future> customGetData() => - MsgHttp.sessionMsg( + Future> customGetData() => + ImGrpc.syncFetchSessionMsgs( talkerId: talkerId, - beginSeqno: msgSeqno != null ? 0 : null, + beginSeqno: msgSeqno != null ? Int64.ZERO : null, endSeqno: msgSeqno, ); } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 5215e99b..5e3c362d 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/grpc/bilibili/im/type.pb.dart' show Msg; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; -import 'package:PiliPlus/models/msg/session.dart'; import 'package:PiliPlus/pages/common/common_publish_page.dart'; import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/pages/whisper_detail/controller.dart'; @@ -110,15 +110,17 @@ class _WhisperDetailPageState _buildBody(_whisperDetailController.loadingState.value)), ), ), - _buildInputView(theme), - buildPanelContainer(theme.colorScheme.onInverseSurface), + if (_whisperDetailController.mid != null) ...[ + _buildInputView(theme), + buildPanelContainer(theme.colorScheme.onInverseSurface), + ], ], ), ), ); } - Widget _buildBody(LoadingState?> loadingState) { + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => loadingWidget, Success() => loadingState.response?.isNotEmpty == true diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index fac3ac04..31536d40 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,10 +1,11 @@ import 'dart:convert'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/grpc/bilibili/im/interfaces/v1.pb.dart' + show EmotionInfo; +import 'package:PiliPlus/grpc/bilibili/im/type.pb.dart' show Msg, MsgType; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; -import 'package:PiliPlus/models/common/msg/msg_type.dart'; -import 'package:PiliPlus/models/msg/session.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -14,9 +15,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class ChatItem extends StatelessWidget { - final MessageItem item; - final List? eInfos; - final VoidCallback? onLongPress; + static MsgType msgTypeFromValue(int value) { + return MsgType.values.firstWhere((e) => e.value == value, + orElse: () => MsgType.EN_INVALID_MSG_TYPE); + } const ChatItem({ super.key, @@ -25,532 +27,35 @@ class ChatItem extends StatelessWidget { this.onLongPress, }) : isOwner = onLongPress != null; + final Msg item; + final List? eInfos; + final VoidCallback? onLongPress; final bool isOwner; @override Widget build(BuildContext context) { - bool isPic = item.msgType == MsgType.pic.value; // 图片 - // bool isText = item.msgType == MsgType.text.value; // 文本 - // bool isArchive = item.msgType == 11; // 投稿 - // bool isArticle = item.msgType == 12; // 专栏 - bool isRevoke = item.msgType == MsgType.revoke.value; // 撤回消息 - // bool isShareV2 = item.msgType == MsgType.share_v2.value; - bool isSystem = item.msgType == MsgType.notifyText.value || - item.msgType == MsgType.notifyMsg.value || - item.msgType == MsgType.picCard.value || - item.msgType == MsgType.autoReplyPush.value; - dynamic content = item.content ?? ''; - final ThemeData theme = Theme.of(context); - Color textColor() { - return isOwner - ? theme.colorScheme.onSecondaryContainer - : theme.colorScheme.onSurfaceVariant; - } + bool isPic = item.msgType == MsgType.EN_MSG_TYPE_PIC.value; // 图片 + bool isRevoke = item.msgType == MsgType.EN_MSG_TYPE_DRAW_BACK.value; // 撤回消息 + bool isSystem = item.msgType == MsgType.EN_MSG_TYPE_TIP_MESSAGE.value || + item.msgType == MsgType.EN_MSG_TYPE_NOTIFY_MSG.value || + item.msgType == MsgType.EN_MSG_TYPE_PICTURE_CARD.value || + item.msgType == MsgType.EN_MSG_TYPE_SYS_GROUP_AUTO_CREATED.value; - Widget richTextMessage(BuildContext context) { - var text = content['content']; - if (eInfos != null) { - final List children = []; - Map emojiMap = {}; - for (var e in eInfos!) { - emojiMap[e['text']] = { - 'url': e['gif_url'] ?? e['url'], - 'size': e['size'] ?? 1, - }; - } - text.splitMapJoin( - RegExp(r"\[[^\[\]]+\]"), - onMatch: (Match match) { - final String emojiKey = match[0]!; - if (emojiMap.containsKey(emojiKey)) { - final double size = 24.0 * emojiMap[emojiKey]!['size']; - children.add(WidgetSpan( - child: NetworkImgLayer( - width: size, - height: size, - src: emojiMap[emojiKey]!['url'], - type: 'emote', - ), - )); - } else { - children.add(TextSpan( - text: emojiKey, - style: TextStyle( - color: textColor(), - letterSpacing: 0.6, - height: 1.5, - ), - )); - } - return ''; - }, - onNonMatch: (String text) { - children.add( - TextSpan( - text: text, - style: TextStyle( - color: textColor(), - letterSpacing: 0.6, - height: 1.5, - ), - ), - ); - return ''; - }, - ); - return SelectableText.rich( - TextSpan( - children: children, - ), - ); - } else { - return SelectableText( - text, - style: TextStyle( - letterSpacing: 0.6, - color: textColor(), - height: 1.5, - ), - ); - } - } + late final ThemeData theme = Theme.of(context); + late final Color textColor = isOwner + ? theme.colorScheme.onSecondaryContainer + : theme.colorScheme.onSurfaceVariant; + late final dynamic content = jsonDecode(item.content); - Widget messageContent(BuildContext context) { - switch (MsgType.parse(item.msgType!)) { - case MsgType.notifyMsg: - return systemNotice(theme); - case MsgType.picCard: - return systemNotice2(); - case MsgType.notifyText: - return Text( - jsonDecode(content['content']) - .map((m) => m['text'] as String) - .join("\n"), - textAlign: TextAlign.center, - style: TextStyle( - letterSpacing: 0.6, - height: 5, - color: theme.colorScheme.outline.withOpacity(0.8), - ), - ); - case MsgType.text: - return richTextMessage(context); - case MsgType.pic: - return GestureDetector( - onTap: () { - context.imageView( - imgList: [ - SourceModel(url: content['url']), - ], - ); - }, - child: Hero( - tag: content['url'], - child: NetworkImgLayer( - width: 220, - height: 220 * content['height'] / content['width'], - src: content['url'], - ), - ), - ); - case MsgType.shareV2: - String? type; - GestureTapCallback onTap; - switch (content['source']) { - // album - case 2: - type = '相簿'; - onTap = () { - PageUtils.pushDynFromId(rid: content['id']); - }; - break; - - // video - case 5: - type = '视频'; - onTap = () async { - dynamic aid = content['id']; - if (aid is String) { - aid = int.tryParse(aid); - } - dynamic bvid = content["bvid"]; - if (aid == null && bvid == null) { - SmartDialog.showToast('null'); - } - bvid ??= IdUtils.av2bv(aid); - SmartDialog.showLoading(); - final int cid = await SearchHttp.ab2c(bvid: bvid); - SmartDialog.dismiss().then( - (e) => PageUtils.toVideoPage( - 'bvid=$bvid&cid=$cid', - arguments: { - 'pic': content['thumb'], - 'heroTag': Utils.makeHeroTag(bvid), - }, - ), - ); - }; - break; - - // article - case 6: - type = '专栏'; - onTap = () { - Get.toNamed( - '/articlePage', - parameters: { - 'id': '${content['id']}', - 'type': 'read', - }, - ); - }; - break; - - // dynamic - case 11: - type = '动态'; - onTap = () { - PageUtils.pushDynFromId(id: content['id']); - }; - break; - - // pgc - case 16: - onTap = () { - PageUtils.viewBangumi(epId: content['id']); - }; - break; - - default: - onTap = () { - SmartDialog.showToast( - 'unsupported source type: ${content['source']}'); - }; - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: onTap, - child: NetworkImgLayer( - width: 220, - height: 220 * 9 / 16, - src: content['thumb'], - ), - ), - const SizedBox(height: 6), - Text( - content['title'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - if (content['source'] == 6 && - (content['headline'] as String?)?.isNotEmpty == true) ...[ - const SizedBox(height: 1), - Text( - content['headline'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - ], - if (content['author'] != null) ...[ - const SizedBox(height: 1), - Text( - '${content['author']}${type != null ? ' · $type' : ''}', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - ), - ), - ], - ], - ); - case MsgType.archiveCard: - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () async { - try { - SmartDialog.showLoading(); - var bvid = content["bvid"]; - final int cid = await SearchHttp.ab2c(bvid: bvid); - SmartDialog.dismiss().then( - (_) => PageUtils.toVideoPage( - 'bvid=$bvid&cid=$cid', - arguments: { - 'pic': content['thumb'], - 'heroTag': Utils.makeHeroTag(bvid), - }, - ), - ); - } catch (err) { - SmartDialog.dismiss(); - SmartDialog.showToast(err.toString()); - } - }, - child: NetworkImgLayer( - width: 220, - height: 220 * 9 / 16, - src: content['cover'], - ), - ), - const SizedBox(height: 6), - SelectableText( - content['title'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 1), - Text( - Utils.timeFormat(content['times']), - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - ), - ), - ], - ); - case MsgType.autoReplyPush: - return Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer.withOpacity(0.4), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - bottomLeft: Radius.circular(6), - bottomRight: Radius.circular(16), - ), - ), - margin: const EdgeInsets.all(12), - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - content['main_title'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - for (var i in content['sub_cards']) ...[ - const SizedBox(height: 6), - GestureDetector( - onTap: () async { - RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', - caseSensitive: false); - Iterable matches = - bvRegex.allMatches(i['jump_url']); - if (matches.isNotEmpty) { - Match match = matches.first; - String bvid = match.group(0)!; - try { - SmartDialog.showLoading(); - final int cid = await SearchHttp.ab2c(bvid: bvid); - SmartDialog.dismiss().then( - (e) => PageUtils.toVideoPage( - 'bvid=$bvid&cid=$cid', - arguments: { - 'pic': i['cover_url'], - 'heroTag': Utils.makeHeroTag(bvid), - }), - ); - } catch (err) { - SmartDialog.dismiss(); - SmartDialog.showToast(err.toString()); - } - } else { - SmartDialog.showToast('未匹配到 BV 号'); - PageUtils.handleWebview(i['jump_url']); - } - }, - child: Row( - children: [ - NetworkImgLayer( - width: 130, - height: 130 * 9 / 16, - src: i['cover_url'], - ), - const SizedBox(width: 6), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i['field1'], - maxLines: 2, - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - Text( - i['field2'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - ), - ), - Text( - i['field3'], - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - ), - ), - ], - )), - ], - )), - ], - ], - )); - case MsgType.articleCard: - return GestureDetector( - onTap: () { - Get.toNamed( - '/articlePage', - parameters: { - 'id': '${content['rid']}', - 'type': "read", - }, - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - for (var i in content['image_urls']) - NetworkImgLayer( - width: 130, - height: 130 * 9 / 16, - src: i, - ), - ], - ), - const SizedBox(height: 6), - SelectableText( - content['title'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 1), - SelectableText( - content['summary'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), - maxLines: 2, - ), - ], - ), - ); - case MsgType.commonShare: - if (content['source'] == '直播') { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - Get.toNamed('/liveRoom?roomid=${content['sourceID']}'); - }, - child: NetworkImgLayer( - width: 220, - height: 220 * 9 / 16, - src: content['cover'], - ), - ), - const SizedBox(height: 6), - Text( - content['title'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 1), - Text( - '${content['author']} · 直播', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor().withOpacity(0.6), - fontSize: 12, - ), - ), - ], - ); - } else { - return Text( - content != null && content != '' - ? (content['content'] ?? content.toString()) - : '不支持的消息类型', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ); - } - default: - return Text( - content != null && content != '' - ? (content['content'] ?? content.toString()) - : '不支持的消息类型', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor(), - fontWeight: FontWeight.bold, - ), - ); - } - } - - return isSystem - ? messageContent(context) - : isRevoke - ? const SizedBox.shrink() + return isRevoke + ? const SizedBox.shrink() + : isSystem + ? messageContent( + context: context, + theme: theme, + content: content, + textColor: textColor, + ) : GestureDetector( onLongPress: () { Feedback.forLongPress(context); @@ -558,12 +63,9 @@ class ChatItem extends StatelessWidget { }, child: Row( children: [ - if (!isOwner) const SizedBox(width: 12), - if (isOwner) const Spacer(), + if (!isOwner) const SizedBox(width: 12) else const Spacer(), Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), + constraints: const BoxConstraints(maxWidth: 300.0), decoration: BoxDecoration( color: isOwner ? theme.colorScheme.secondaryContainer @@ -587,13 +89,18 @@ class ChatItem extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - messageContent(context), + messageContent( + context: context, + theme: theme, + content: content, + textColor: textColor, + ), SizedBox(height: isPic ? 7 : 2), Row( mainAxisSize: MainAxisSize.min, children: [ Text( - Utils.dateFormat(item.timestamp), + Utils.dateFormat(item.timestamp.toInt()), style: theme.textTheme.labelSmall!.copyWith( color: isOwner ? theme.colorScheme.onSecondaryContainer @@ -613,21 +120,509 @@ class ChatItem extends StatelessWidget { ], ), ), - if (!isOwner) const Spacer(), - if (isOwner) const SizedBox(width: 12), + if (!isOwner) const Spacer() else const SizedBox(width: 12), ], ), ); } - Widget systemNotice(ThemeData theme) { + Widget messageContent({ + required BuildContext context, + required ThemeData theme, + required content, + required Color textColor, + }) { + try { + switch (msgTypeFromValue(item.msgType)) { + case MsgType.EN_MSG_TYPE_NOTIFY_MSG: + return msgTypeNotifyMsg_10(theme, content); + case MsgType.EN_MSG_TYPE_PICTURE_CARD: + return msgTypePictureCard_13(content); + case MsgType.EN_MSG_TYPE_TIP_MESSAGE: + return msgTypeTipMessage_18(theme, content); + case MsgType.EN_MSG_TYPE_TEXT: + return msgTypeText_1(content: content, textColor: textColor); + case MsgType.EN_MSG_TYPE_PIC: + return msgTypePic_2(context, content); + case MsgType.EN_MSG_TYPE_SHARE_V2: + return msgTypeShareV2_7(content, textColor); + case MsgType.EN_MSG_TYPE_VIDEO_CARD: + return msgTypeVideoCard_11(content, textColor); + case MsgType.EN_MSG_TYPE_SYS_GROUP_AUTO_CREATED: + return msgTypeSysGroupAutoCreated_208(theme, content, textColor); + case MsgType.EN_MSG_TYPE_ARTICLE_CARD: + return msgTypeArticleCard_12(content, textColor); + case MsgType.EN_MSG_TYPE_COMMON_SHARE_CARD: + return msgTypeCommonShareCard_14(content, textColor); + default: + return def(textColor); + } + } catch (err) { + return def(textColor, err: err); + } + } + + Widget msgTypeCommonShareCard_14(content, Color textColor) { + if (content['source'] == '直播') { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Get.toNamed('/liveRoom?roomid=${content['sourceID']}'); + }, + child: NetworkImgLayer( + width: 220, + height: 220 * 9 / 16, + src: content['cover'], + ), + ), + const SizedBox(height: 6), + Text( + content['title'] ?? "", + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 1), + Text( + '${content['author']} · 直播', + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ); + } else { + return def(textColor); + } + } + + Widget msgTypeArticleCard_12(content, Color textColor) { + return GestureDetector( + onTap: () { + Get.toNamed( + '/articlePage', + parameters: { + 'id': '${content['rid']}', + 'type': "read", + }, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + for (var i in content['image_urls']) + NetworkImgLayer( + width: 130, + height: 130 * 9 / 16, + src: i, + ), + ], + ), + const SizedBox(height: 6), + SelectableText( + content['title'] ?? "", + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 1), + SelectableText( + content['summary'] ?? "", + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + maxLines: 2, + ), + ], + ), + ); + } + + Widget msgTypeSysGroupAutoCreated_208( + ThemeData theme, content, Color textColor) { + return Container( + constraints: const BoxConstraints(maxWidth: 300.0), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer.withOpacity(0.4), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(16), + ), + ), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + content['main_title'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + for (var i in content['sub_cards']) ...[ + const SizedBox(height: 6), + GestureDetector( + onTap: () async { + RegExp bvRegex = + RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false); + Iterable matches = bvRegex.allMatches(i['jump_url']); + if (matches.isNotEmpty) { + Match match = matches.first; + String bvid = match.group(0)!; + try { + SmartDialog.showLoading(); + final int cid = await SearchHttp.ab2c(bvid: bvid); + SmartDialog.dismiss().then( + (e) => PageUtils.toVideoPage('bvid=$bvid&cid=$cid', + arguments: { + 'pic': i['cover_url'], + 'heroTag': Utils.makeHeroTag(bvid), + }), + ); + } catch (err) { + SmartDialog.dismiss(); + SmartDialog.showToast(err.toString()); + } + } else { + SmartDialog.showToast('未匹配到 BV 号'); + PageUtils.handleWebview(i['jump_url']); + } + }, + child: Row( + children: [ + NetworkImgLayer( + width: 130, + height: 130 * 9 / 16, + src: i['cover_url'], + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i['field1'], + maxLines: 2, + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + Text( + i['field2'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + ), + ), + Text( + i['field3'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ], + ), + ); + } + + Widget msgTypeVideoCard_11(content, Color textColor) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () async { + try { + SmartDialog.showLoading(); + var bvid = content["bvid"]; + final int cid = await SearchHttp.ab2c(bvid: bvid); + SmartDialog.dismiss().then( + (_) => PageUtils.toVideoPage( + 'bvid=$bvid&cid=$cid', + arguments: { + 'pic': content['thumb'], + 'heroTag': Utils.makeHeroTag(bvid), + }, + ), + ); + } catch (err) { + SmartDialog.dismiss(); + SmartDialog.showToast(err.toString()); + } + }, + child: NetworkImgLayer( + width: 220, + height: 220 * 9 / 16, + src: content['cover'], + ), + ), + const SizedBox(height: 6), + SelectableText( + content['title'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 1), + Text( + Utils.timeFormat(content['times']), + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ); + } + + Widget msgTypeShareV2_7(content, Color textColor) { + String? type; + GestureTapCallback onTap; + switch (content['source']) { + // album + case 2: + type = '相簿'; + onTap = () { + PageUtils.pushDynFromId(rid: content['id']); + }; + break; + + // video + case 5: + type = '视频'; + onTap = () async { + dynamic aid = content['id']; + if (aid is String) { + aid = int.tryParse(aid); + } + dynamic bvid = content["bvid"]; + if (aid == null && bvid == null) { + SmartDialog.showToast('null'); + } + bvid ??= IdUtils.av2bv(aid); + SmartDialog.showLoading(); + final int cid = await SearchHttp.ab2c(bvid: bvid); + SmartDialog.dismiss().then( + (e) => PageUtils.toVideoPage( + 'bvid=$bvid&cid=$cid', + arguments: { + 'pic': content['thumb'], + 'heroTag': Utils.makeHeroTag(bvid), + }, + ), + ); + }; + break; + + // article + case 6: + type = '专栏'; + onTap = () { + Get.toNamed( + '/articlePage', + parameters: { + 'id': '${content['id']}', + 'type': 'read', + }, + ); + }; + break; + + // dynamic + case 11: + type = '动态'; + onTap = () { + PageUtils.pushDynFromId(id: content['id']); + }; + break; + + // pgc + case 16: + onTap = () { + PageUtils.viewBangumi(epId: content['id']); + }; + break; + + default: + onTap = () { + SmartDialog.showToast( + 'unsupported source type: ${content['source']}'); + }; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: onTap, + child: NetworkImgLayer( + width: 220, + height: 220 * 9 / 16, + src: content['thumb'], + ), + ), + const SizedBox(height: 6), + Text( + content['title'] ?? "", + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + if (content['source'] == 6 && + (content['headline'] as String?)?.isNotEmpty == true) ...[ + const SizedBox(height: 1), + Text( + content['headline'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + ], + if (content['author'] != null) ...[ + const SizedBox(height: 1), + Text( + '${content['author']}${type != null ? ' · $type' : ''}', + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ], + ); + } + + Widget msgTypePic_2(BuildContext context, content) { + return GestureDetector( + onTap: () { + context.imageView(imgList: [SourceModel(url: content['url'])]); + }, + child: Hero( + tag: content['url'], + child: NetworkImgLayer( + width: 220, + height: 220 * content['height'] / content['width'], + src: content['url'], + ), + ), + ); + } + + Widget msgTypeTipMessage_18(ThemeData theme, content) { + return Text( + jsonDecode(content['content']).map((e) => e['text']).join("\n"), + textAlign: TextAlign.center, + style: TextStyle( + letterSpacing: 0.6, + height: 5, + color: theme.colorScheme.outline.withOpacity(0.8), + ), + ); + } + + Widget msgTypeText_1({ + required content, + required Color textColor, + }) { + late final style = TextStyle( + color: textColor, + letterSpacing: 0.6, + height: 1.5, + ); + if (eInfos != null) { + final List children = []; + Map emojiMap = {}; + for (var e in eInfos!) { + emojiMap[e.text] = { + 'url': e.hasGifUrl() ? e.gifUrl : e.url, + 'size': e.size * 24.0, + }; + } + content['content'].splitMapJoin( + RegExp(r"\[[^\[\]]+\]"), + onMatch: (Match match) { + final String emojiKey = match[0]!; + if (emojiMap.containsKey(emojiKey)) { + children.add( + WidgetSpan( + child: NetworkImgLayer( + width: emojiMap[emojiKey]!['size'], + height: emojiMap[emojiKey]!['size'], + src: emojiMap[emojiKey]!['url'], + type: 'emote', + ), + ), + ); + } else { + children.add(TextSpan(text: emojiKey, style: style)); + } + return ''; + }, + onNonMatch: (String text) { + children.add(TextSpan(text: text, style: style)); + return ''; + }, + ); + return SelectableText.rich(TextSpan(children: children)); + } + return SelectableText(content['content'], style: style); + } + + Widget msgTypeNotifyMsg_10(ThemeData theme, content) { return Row( children: [ const SizedBox(width: 12), Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), + constraints: const BoxConstraints(maxWidth: 300.0), decoration: BoxDecoration( color: theme.colorScheme.secondaryContainer.withOpacity(0.4), borderRadius: const BorderRadius.only( @@ -642,46 +637,53 @@ class ChatItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SelectableText(item.content['title'], - style: theme.textTheme.titleMedium! - .copyWith(fontWeight: FontWeight.bold)), + SelectableText( + content['title'], + style: theme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + ), + ), Text( - Utils.dateFormat(item.timestamp), + Utils.dateFormat(item.timestamp.toInt()), style: theme.textTheme.labelSmall! .copyWith(color: theme.colorScheme.outline), ), - Divider( - color: theme.colorScheme.primary.withOpacity(0.05), - ), - SelectableText( - item.content['text'], - ) + Divider(color: theme.colorScheme.primary.withOpacity(0.05)), + SelectableText(content['text']) ], ), ), - const Spacer(), ], ); } - Widget systemNotice2() { + Widget msgTypePictureCard_13(content) { return Row( children: [ const SizedBox(width: 12), Container( - constraints: const BoxConstraints( - maxWidth: 300.0, // 设置最大宽度为200.0 - ), + constraints: const BoxConstraints(maxWidth: 300.0), margin: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(bottom: 6), child: NetworkImgLayer( width: 320, height: 150, - src: item.content['pic_url'], + src: content['pic_url'], ), ), - const Spacer(), ], ); } + + Widget def(Color textColor, {err}) { + return Text( + '${item.content}${err != null ? '\n\ntype: ${msgTypeFromValue(item.msgType)}\nerr: $err' : ''}', + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), + ); + } }