import 'dart:async'; import 'dart:io' show File; import 'package:PiliPlus/common/widgets/chat_list_view.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.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/image_type.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart'; import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/pages/whisper_detail/controller.dart'; import 'package:PiliPlus/pages/whisper_detail/widget/chat_item.dart'; import 'package:PiliPlus/pages/whisper_link_setting/view.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/widget_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide TextField; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; class WhisperDetailPage extends CommonRichTextPubPage { const WhisperDetailPage({ super.key, super.autofocus = false, }); @override State createState() => _WhisperDetailPageState(); } class _WhisperDetailPageState extends CommonRichTextPubPageState { final _whisperDetailController = Get.put( WhisperDetailController(), tag: Utils.makeHeroTag(Get.parameters['talkerId']), ); @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final padding = MediaQuery.viewPaddingOf(context); late final containerColor = ElevationOverlay.colorWithOverlay( theme.colorScheme.surface, theme.hoverColor, 1, ); return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( title: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (_whisperDetailController.mid != null) { feedBack(); Get.toNamed('/member?mid=${_whisperDetailController.mid}'); } }, child: Row( mainAxisSize: MainAxisSize.min, children: [ NetworkImgLayer( width: 34, height: 34, type: ImageType.avatar, src: _whisperDetailController.face, ), const SizedBox(width: 6), Flexible( child: Text( _whisperDetailController.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(height: 1, fontSize: 16), strutStyle: const StrutStyle( leading: 0, height: 1, fontSize: 16, ), ), ), if (_whisperDetailController.isLive) ...[ const SizedBox(width: 10), Image.asset( 'assets/images/live/live.gif', height: 16, cacheHeight: 16.cacheSize(context), filterQuality: FilterQuality.low, ), ], ], ), ), actions: [ IconButton( tooltip: '设置', onPressed: () => Get.to( WhisperLinkSettingPage( talkerUid: _whisperDetailController.talkerId, ), ), icon: Icon( size: 22, Icons.settings, color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.8), ), ), const SizedBox(width: 5), ], ), body: Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), child: Column( children: [ Expanded( child: Listener( onPointerDown: hidePanel, behavior: HitTestBehavior.opaque, child: Align( alignment: Alignment.topCenter, child: Obx( () => _buildBody(_whisperDetailController.loadingState.value), ), ), ), ), if (_whisperDetailController.mid != null) ...[ _buildInputView(theme, containerColor), buildPanelContainer( theme, containerColor, ), ] else SizedBox(height: padding.bottom), ], ), ).constraintWidth(), ); } Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => loadingWidget, Success(:final response) => response != null && response.isNotEmpty ? ChatListView.separated( itemCount: response.length, padding: const EdgeInsets.all(14), physics: const AlwaysScrollableScrollPhysics( parent: ClampingScrollPhysics(), ), controller: _whisperDetailController.scrollController, itemBuilder: (context, int index) { if (index == response.length - 1) { _whisperDetailController.onLoadMore(); } final item = response[index]; final isOwner = item.senderUid.toInt() == _whisperDetailController.account.mid; return ChatItem( item: item, eInfos: _whisperDetailController.eInfos, onLongPress: () => onLongPress(index, item, isOwner), onSecondaryTapUp: PlatformUtils.isDesktop ? (e) => _showMenu(e.globalPosition, index, item, isOwner) : null, isOwner: isOwner, ); }, separatorBuilder: (context, index) => const SizedBox(height: 12), ) : scrollErrorWidget(onReload: _whisperDetailController.onReload), Error(:final errMsg) => scrollErrorWidget( errMsg: errMsg, onReload: _whisperDetailController.onReload, ), }; } void _showMenu(Offset offset, int index, Msg item, bool isOwner) { showMenu( context: context, position: PageUtils.menuPosition(offset), items: [ if (isOwner) PopupMenuItem( height: 42, onTap: () => _whisperDetailController.sendMsg( message: '${item.msgKey}', onClearText: editController.clear, msgType: 5, index: index, ), child: const Text('撤回', style: TextStyle(fontSize: 14)), ) else PopupMenuItem( height: 42, onTap: () => autoWrapReportDialog( context, ban: false, ReportOptions.imMsgReport, (reasonType, reasonDesc, banUid) => _whisperDetailController.onReport( item, reasonType, reasonType == 0 ? reasonDesc! : ReportOptions.imMsgReport['']![reasonType]!, ), ), child: const Text('举报', style: TextStyle(fontSize: 14)), ), ], ); } void onLongPress(int index, Msg item, bool isOwner) { Feedback.forLongPress(context); showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: isOwner ? ListTile( onTap: () { Get.back(); _whisperDetailController.sendMsg( message: '${item.msgKey}', onClearText: editController.clear, msgType: 5, index: index, ); }, dense: true, title: const Text('撤回', style: TextStyle(fontSize: 14)), ) : ListTile( onTap: () { Get.back(); autoWrapReportDialog( context, ban: false, ReportOptions.imMsgReport, (reasonType, reasonDesc, banUid) => _whisperDetailController.onReport( item, reasonType, reasonType == 0 ? reasonDesc! : ReportOptions.imMsgReport['']![reasonType]!, ), ); }, dense: true, title: const Text('举报', style: TextStyle(fontSize: 14)), ), ), ); } Widget _buildInputView(ThemeData theme, Color containerColor) { return Container( padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: containerColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ IconButton( onPressed: () => updatePanelType( panelType.value == PanelType.emoji ? PanelType.keyboard : PanelType.emoji, ), icon: const Icon(Icons.emoji_emotions), tooltip: '表情', ), Expanded( child: Listener( onPointerUp: (event) { // Currently it may be emojiPanel. if (readOnly.value) { updatePanelType(PanelType.keyboard); } }, child: Obx( () => RichTextField( key: key, readOnly: readOnly.value, focusNode: focusNode, controller: editController, minLines: 1, maxLines: 4, onChanged: onChanged, onSubmitted: onSubmitted, textInputAction: TextInputAction.newline, decoration: InputDecoration( filled: true, hintText: '发个消息聊聊呗~', fillColor: theme.colorScheme.surface, border: const OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.all(Radius.circular(6)), gapPadding: 0, ), contentPadding: const EdgeInsets.all(10), ), // inputFormatters: [LengthLimitingTextInputFormatter(500)], ), ), ), ), Obx( () { final enablePublish = this.enablePublish.value; return IconButton( onPressed: () async { if (enablePublish) { _whisperDetailController.sendMsg( message: editController.rawText, onClearText: () { editController.clear(); this.enablePublish.value = false; }, ); } else { try { final XFile? pickedFile = await imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 100, ); if (pickedFile != null) { final path = pickedFile.path; SmartDialog.showLoading(msg: '正在上传图片'); final result = await MsgHttp.uploadBfs( path: path, biz: 'im', ); if (result case Success(:final response)) { final mimeType = lookupMimeType( path, )?.split('/').elementAtOrNull(1) ?? 'jpg'; final picMsg = { 'url': response.imageUrl, 'height': response.imageHeight, 'width': response.imageWidth, 'imageType': mimeType, 'original': 1, 'size': response.imgSize, }; SmartDialog.showLoading(msg: '正在发送'); await _whisperDetailController .sendMsg( picMsg: picMsg, onClearText: editController.clear, ) .whenComplete(() { if (PlatformUtils.isMobile) { File(path).tryDel(); } }); } else { SmartDialog.dismiss(); result.toast(); return; } } } catch (e) { SmartDialog.showToast(e.toString()); } } }, icon: Icon( enablePublish ? Icons.send : Icons.add_photo_alternate_outlined, ), tooltip: enablePublish ? '发送' : '图片', ); }, ), ], ), ); } @override Widget? get customPanel => EmotePanel(onChoose: onChooseEmote); @override Future onCustomPublish({List? pictures}) { throw UnimplementedError(); } @override Future? onMention([bool fromClick = false]) => null; @override void onSave() {} }