import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/models/msg/reply.dart'; import 'package:pilipala/pages/message/utils/index.dart'; import 'package:pilipala/utils/utils.dart'; import 'controller.dart'; class MessageReplyPage extends StatefulWidget { const MessageReplyPage({super.key}); @override State createState() => _MessageReplyPageState(); } class _MessageReplyPageState extends State { final MessageReplyController _messageReplyCtr = Get.put(MessageReplyController()); late Future _futureBuilderFuture; final ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); _futureBuilderFuture = _messageReplyCtr.queryMessageReply(); scrollController.addListener( () async { if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 200) { EasyThrottle.throttle('follow', const Duration(seconds: 1), () { _messageReplyCtr.queryMessageReply(type: 'onLoad'); }); } }, ); } @override void dispose() { scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('回复我的')), body: RefreshIndicator( onRefresh: () async { await _messageReplyCtr.queryMessageReply(type: 'init'); }, child: FutureBuilder( future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { Map? data = snapshot.data; if (data != null && data['status']) { final replyItems = _messageReplyCtr.replyItems; return Obx( () => replyItems.isEmpty ? const CustomScrollView(slivers: [NoData()]) : ListView.separated( controller: scrollController, itemBuilder: (context, index) => ReplyItem(item: replyItems[index]), itemCount: replyItems.length, separatorBuilder: (BuildContext context, int index) { return Divider( indent: 66, endIndent: 14, height: 1, color: Colors.grey.withOpacity(0.1), ); }, ), ); } else { // 请求错误 return HttpError( errMsg: data?['msg'] ?? '请求异常', fn: () { setState(() { _futureBuilderFuture = _messageReplyCtr.queryMessageReply(); }); }, isInSliver: false, ); } } else { return const SizedBox(); } }, ), ), ); } } class ReplyItem extends StatelessWidget { final MessageReplyItem item; const ReplyItem({super.key, required this.item}); @override Widget build(BuildContext context) { Color outline = Theme.of(context).colorScheme.outline; final String heroTag = Utils.makeHeroTag(item.user!.mid); final Uri uri = Uri.parse(item.item!.uri!); /// bilibili:// final Uri nativeUri = Uri.parse(item.item!.nativeUri!); final String type = item.item!.type!; return InkWell( onTap: () async { MessageUtils.onClickMessage(context, uri, nativeUri, type, item.item!); }, child: Padding( padding: const EdgeInsets.all(14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( onTap: () { Get.toNamed('/member?mid=${item.user!.mid}', arguments: {'face': item.user!.avatar, 'heroTag': heroTag}); }, child: Hero( tag: heroTag, child: NetworkImgLayer( width: 42, height: 42, type: 'avatar', src: item.user!.avatar, ), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text.rich(TextSpan(children: [ TextSpan(text: item.user!.nickname!), const TextSpan(text: ' '), if (item.item!.type! == 'video') TextSpan( text: '对我的视频发表了评论', style: TextStyle(color: outline)), if (item.item!.type! == 'reply') TextSpan( text: '回复了我的评论', style: TextStyle(color: outline), ), if (item.item!.type! == 'dynamic') TextSpan( text: '对我的动态发表了评论', style: TextStyle(color: outline)), ])), const SizedBox(height: 6), Text.rich( maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(letterSpacing: 0.3), buildContent(context, item.item)), if (item.item!.targetReplyContent != '') ...[ const SizedBox(height: 2), Text( item.item!.targetReplyContent!, style: TextStyle(color: outline), ), ], const SizedBox(height: 4), Row( children: [ Text( Utils.dateFormat(item.replyTime!, formatType: 'detail'), style: TextStyle(color: outline), ), const SizedBox(width: 16), // Text('回复', style: TextStyle(color: outline)), ], ) ], ), ), const SizedBox(width: 25), if (item.item!.type! == 'reply') Container( width: 60, height: 80, padding: const EdgeInsets.all(4), child: Text( item.item!.rootReplyContent!, maxLines: 4, style: const TextStyle(fontSize: 12, letterSpacing: 0.3), overflow: TextOverflow.ellipsis, ), ), if (item.item!.type! == 'video') // NetworkImgLayer( // width: 60, // height: 60, // src: item.item!.image, // radius: 6, // ), Container( width: 60, height: 60, clipBehavior: Clip.hardEdge, decoration: const BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(6)), ), child: Image.network(item.item!.image!, fit: BoxFit.cover), ), if (item.item!.type! == 'dynamic') Container( width: 60, height: 80, padding: const EdgeInsets.all(4), child: Text( item.item!.title!, maxLines: 4, style: const TextStyle(fontSize: 12, letterSpacing: 0.3), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } } InlineSpan buildContent(BuildContext context, item) { List? atDetails = item!.atDetails; final List spanChilds = []; if (atDetails!.isNotEmpty) { final String patternStr = atDetails.map((e) => '@${e['nickname']}').toList().join('|'); final RegExp regExp = RegExp(patternStr); item.sourceContent!.splitMapJoin( regExp, onMatch: (Match match) { spanChilds.add( TextSpan( text: match.group(0), recognizer: TapGestureRecognizer() ..onTap = () { var currentUser = atDetails .where((e) => e['nickname'] == match.group(0)!.substring(1)) .first; Get.toNamed('/member?mid=${currentUser['mid']}', arguments: { 'face': currentUser['avatar'], }); }, style: TextStyle(color: Theme.of(context).colorScheme.primary), ), ); return ''; }, onNonMatch: (String nonMatch) { spanChilds.add( TextSpan(text: nonMatch), ); return ''; }, ); } else { spanChilds.add( TextSpan(text: item.sourceContent), ); } return TextSpan(children: spanChilds); }