Files
PiliPlus/lib/pages/video/detail/reply/widgets/reply_item_grpc.dart
bggRGjQaUbCoE e1ab9e19cb feat: save reply
Closes #614

opt: more panel

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-04-07 01:23:00 +08:00

1340 lines
47 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/imageview.dart';
import 'package:PiliPlus/common/widgets/report.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_save.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/zan_grpc.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/models/common/reply_type.dart';
import 'package:PiliPlus/pages/video/detail/index.dart';
import 'package:PiliPlus/utils/feed_back.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/url_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:html/parser.dart' show parse;
class ReplyItemGrpc extends StatelessWidget {
const ReplyItemGrpc({
super.key,
required this.replyItem,
this.replyLevel,
this.showReplyRow = true,
this.replyReply,
this.replyType,
this.needDivider = true,
this.onReply,
this.onDelete,
this.upMid,
this.showDialogue,
this.getTag,
this.onViewImage,
this.onDismissed,
this.callback,
this.onCheckReply,
this.onToggleTop,
});
final ReplyInfo replyItem;
final String? replyLevel;
final bool showReplyRow;
final Function? replyReply;
final ReplyType? replyType;
final bool needDivider;
final Function()? onReply;
final Function(dynamic rpid, dynamic frpid)? onDelete;
final dynamic upMid;
final VoidCallback? showDialogue;
final Function? getTag;
final VoidCallback? onViewImage;
final ValueChanged<int>? onDismissed;
final Function(List<String>, int)? callback;
final ValueChanged<ReplyInfo>? onCheckReply;
final Function(bool isUpTop, int rpid)? onToggleTop;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
// 点击整个评论区 评论详情/回复
onTap: () {
feedBack();
replyReply?.call(replyItem, null);
},
onLongPress: () {
feedBack();
// showDialog(
// context: context,
// builder: (context) => AlertDialog(
// content: SelectableText(jsonEncode(replyItem.toProto3Json())),
// ),
// );
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return morePanel(
context: context,
item: replyItem,
onDelete: (rpid) {
onDelete?.call(rpid, null);
},
isSubReply: false,
);
},
);
},
child: _buildContent(context),
),
);
}
Widget _buildAuthorPanel(context) => Padding(
padding: const EdgeInsets.fromLTRB(12, 14, 8, 5),
child: content(context),
);
Widget _buildContent(context) {
return Column(
children: [
if (ModuleAuthorModel.showDynDecorate &&
replyItem.member.hasGarbCardImage())
Stack(
clipBehavior: Clip.none,
children: [
Positioned(
top: 8,
right: 12,
// child: GestureDetector(
// onTap: replyItem.member.garbCardJumpUrl.isNotEmpty
// ? () {
// Get.toNamed(
// 'webview',
// parameters: {
// 'url': replyItem.member.garbCardJumpUrl
// },
// );
// }
// : null,
child: Stack(
alignment: Alignment.centerRight,
children: [
CachedNetworkImage(
height: 38,
imageUrl: replyItem.member.garbCardImage,
),
if (replyItem.member.hasGarbCardNumber())
Text(
'NO.\n${replyItem.member.garbCardNumber}',
style: replyItem.member.garbCardFanColor.startsWith('#')
? TextStyle(
fontSize: 8,
fontFamily: 'digital_id_num',
color: Color(
int.parse(
replyItem.member.garbCardFanColor
.replaceFirst('#', '0xFF'),
),
),
)
: null,
),
],
),
),
// ),
_buildAuthorPanel(context),
],
)
else
_buildAuthorPanel(context),
if (needDivider)
Divider(
indent: 55,
endIndent: 15,
height: 0.3,
color: Theme.of(context).colorScheme.outline.withOpacity(0.08),
)
],
);
}
Widget lfAvtar(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
if (ModuleAuthorModel.showDynDecorate &&
replyItem.member.hasGarbPendantImage()) ...[
Padding(
padding: const EdgeInsets.all(2),
child: NetworkImgLayer(
src: replyItem.member.face,
width: 30,
height: 30,
type: 'avatar',
),
),
Positioned(
left: -9,
top: -9,
child: IgnorePointer(
child: CachedNetworkImage(
width: 52,
height: 52,
imageUrl: replyItem.member.garbPendantImage,
),
),
),
] else
NetworkImgLayer(
src: replyItem.member.face,
width: 34,
height: 34,
type: 'avatar',
),
if (replyItem.member.vipStatus > 0)
Positioned(
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
//borderRadius: BorderRadius.circular(7),
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: Image.asset(
'assets/images/big-vip.png',
height: 14,
semanticLabel: "大会员",
),
),
),
//https://www.bilibili.com/blackboard/activity-whPrHsYJ2.html
if (replyItem.member.officialVerifyType == 0)
Positioned(
left: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(8),
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: const Icon(
Icons.offline_bolt,
color: Colors.yellow,
size: 14,
semanticLabel: "认证个人",
),
),
)
else if (replyItem.member.officialVerifyType == 1)
Positioned(
left: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(8),
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: const Icon(
Icons.offline_bolt,
color: Colors.lightBlueAccent,
size: 14,
semanticLabel: "认证机构",
),
),
),
],
);
}
Widget content(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
/// fix Stack内GestureDetector onTap无效
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
feedBack();
Get.toNamed('/member?mid=${replyItem.mid}');
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
lfAvtar(context),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
replyItem.member.name,
style: TextStyle(
color: (replyItem.member.vipStatus > 0 &&
replyItem.member.vipType == 2)
? context.vipColor
: Theme.of(context).colorScheme.outline,
fontSize: 13,
),
),
const SizedBox(width: 6),
Image.asset(
'assets/images/lv/lv${replyItem.member.level}.png',
height: 11,
semanticLabel: "等级:${replyItem.member.level}",
),
const SizedBox(width: 6),
if (replyItem.mid == upMid)
const PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
Utils.dateFormat(replyItem.ctime.toInt()),
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
if (replyItem.replyControl.location.isNotEmpty)
Text(
'${replyItem.replyControl.location}',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
),
],
)
],
),
],
),
),
// title
Padding(
padding: EdgeInsets.only(
top: 10,
left: replyLevel == '' ? 6 : 45,
right: 6,
bottom: 4,
),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
String text = replyItem.content.message;
TextStyle style = TextStyle(
height: 1.75,
fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize,
);
TextPainter? textPainter;
bool? didExceedMaxLines;
if (replyLevel == '1' && GlobalData().replyLengthLimit != 0) {
textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: GlobalData().replyLengthLimit,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth);
didExceedMaxLines = textPainter.didExceedMaxLines;
}
return Semantics(
label: text,
child: Text.rich(
style: style,
TextSpan(
children: [
if (replyItem.replyControl.isUpTop) ...[
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
semanticsLabel: '置顶',
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),
],
buildContent(
context,
replyItem,
replyReply,
null,
textPainter,
didExceedMaxLines,
),
],
),
),
);
},
),
),
// 操作区域
if (replyLevel != '') buttonAction(context, replyItem.replyControl),
// 一楼的评论
if (showReplyRow &&
( //replyItem.replyControl!.isShow! ||
replyItem.replies.isNotEmpty ||
replyItem.replyControl.subReplyEntryText.isNotEmpty)) ...[
Padding(
padding: const EdgeInsets.only(top: 5, bottom: 12),
child: replyItemRow(
context: context,
),
),
],
],
);
}
get _style => TextButton.styleFrom(
padding: const EdgeInsets.all(0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity(horizontal: -2, vertical: -2),
);
// 感谢、回复、复制
Widget buttonAction(BuildContext context, replyControl) {
return Row(
children: <Widget>[
const SizedBox(width: 36),
SizedBox(
height: 32,
child: TextButton(
style: _style,
onPressed: () {
feedBack();
onReply?.call();
},
child: Row(children: [
Icon(
Icons.reply,
size: 18,
color: Theme.of(context).colorScheme.outline.withOpacity(0.8),
),
const SizedBox(width: 3),
Text(
'回复',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
]),
),
),
const SizedBox(width: 2),
if (replyItem.replyControl.upLike) ...[
SizedBox(
height: 32,
child: TextButton(
onPressed: null,
style: _style,
child: Text(
'UP主觉得很赞',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
fontWeight: FontWeight.normal,
),
),
),
),
const SizedBox(width: 2),
],
if (replyItem.replyControl.cardLabels
.map((item) => item.textContent)
.toList()
.contains('热评'))
Text(
'热评',
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize),
),
if (replyLevel == '2' &&
needDivider &&
replyItem.id != replyItem.dialog)
SizedBox(
height: 32,
child: TextButton(
onPressed: showDialogue,
style: _style,
child: Text(
'查看对话',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
fontWeight: FontWeight.normal,
),
),
),
),
const Spacer(),
ZanButtonGrpc(replyItem: replyItem, replyType: replyType),
const SizedBox(width: 5)
],
);
}
Widget replyItemRow({required BuildContext context}) {
final bool extraRow = replyItem.replies.length < replyItem.count.toInt();
return Container(
margin: const EdgeInsets.only(left: 42, right: 4, top: 0),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
animationDuration: Duration.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (replyItem.replies.isNotEmpty)
for (int i = 0; i < replyItem.replies.length; i++) ...[
InkWell(
// 一楼点击评论展开评论详情
onTap: () => replyReply?.call(
replyItem, replyItem.replies[i].id.toInt()),
onLongPress: () {
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return morePanel(
context: context,
item: replyItem.replies[i],
onDelete: (rpid) {
onDelete?.call(rpid, replyItem.id.toInt());
},
isSubReply: true,
);
},
);
},
child: Container(
width: double.infinity,
padding: EdgeInsets.fromLTRB(
8,
i == 0 && (extraRow || replyItem.replies.length > 1)
? 8
: 4,
8,
i == 0 && (extraRow || replyItem.replies.length > 1)
? 4
: 6,
),
child: Semantics(
label:
'${replyItem.replies[i].member.name} ${replyItem.replies[i].content.message}',
excludeSemantics: true,
child: Text.rich(
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.bodyMedium!
.fontSize,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.85),
height: 1.6),
overflow: TextOverflow.ellipsis,
maxLines: 2,
TextSpan(
children: [
TextSpan(
text: replyItem.replies[i].member.name,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
feedBack();
Get.toNamed(
'/member?mid=${replyItem.replies[i].member.mid}',
);
},
),
if (replyItem.replies[i].mid == upMid) ...[
const TextSpan(text: ' '),
const WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
textScaleFactor: 1,
),
),
const TextSpan(text: ' '),
],
TextSpan(
text: replyItem.replies[i].root ==
replyItem.replies[i].parent
? ': '
: replyItem.replies[i].mid == upMid
? ''
: ' ',
),
buildContent(
context,
replyItem.replies[i],
replyReply,
replyItem,
null,
null,
),
],
),
),
),
),
)
],
if (extraRow)
InkWell(
// 一楼点击【共xx条回复】展开评论详情
onTap: () => replyReply?.call(replyItem, null),
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),
child: Text.rich(
TextSpan(
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
children: [
if (replyItem.replyControl.upReply)
TextSpan(
text: 'UP主等人 ',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.85),
),
),
TextSpan(
text: replyItem.replyControl.subReplyEntryText,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
)
],
),
),
),
)
],
),
),
);
}
InlineSpan buildContent(
BuildContext context,
replyItem,
replyReply,
fReplyItem,
textPainter,
didExceedMaxLines,
) {
final String routePath = Get.currentRoute;
bool isVideoPage = routePath.startsWith('/video');
// replyItem 当前回复内容
// replyReply 查看二楼回复(回复详情)回调
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final Content content = replyItem.content;
String message = content.message;
final List<InlineSpan> spanChildren = <InlineSpan>[];
if (didExceedMaxLines == true) {
final textSize = textPainter.size;
final double maxHeight = textPainter.preferredLineHeight * 6;
var position = textPainter.getPositionForOffset(
Offset(
textSize.width,
maxHeight, // textSize.height,
),
);
// final endOffset = textPainter.getOffsetBefore(position.offset);
message = message.substring(0, position.offset);
}
// return TextSpan(text: message);
// 投票
if (content.hasVote()) {
message.splitMapJoin(RegExp(r"\{vote:\d+?\}"), onMatch: (Match match) {
// String matchStr = match[0]!;
spanChildren.add(
TextSpan(
text: '投票: ${content.vote.title}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/webview',
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${content.vote.id}',
},
);
},
),
);
return '';
}, onNonMatch: (String str) {
return str;
});
message = message.replaceAll(RegExp(r"\{vote:\d+?\}"), "");
}
message = parse(message).body?.text ?? message;
// .replaceAll('&amp;', '&')
// .replaceAll('&lt;', '<')
// .replaceAll('&gt;', '>')
// .replaceAll('&quot;', '"')
// .replaceAll('&apos;', "'")
// .replaceAll('&nbsp;', ' ');
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
...content.topic.keys.map((e) => '#$e#'),
...content.atNameToMid.keys.map((e) => '@$e'),
];
List<String> jumpUrlKeysList = content.url.keys.map<String>((String e) {
return e;
}).toList();
specialTokens.sort((a, b) => b.length.compareTo(a.length));
String patternStr = specialTokens.map(RegExp.escape).join('|');
if (patternStr.isNotEmpty) {
patternStr += "|";
}
patternStr += r'(\b(?:\d+[:])?\d+[:]\d+\b)';
if (jumpUrlKeysList.isNotEmpty) {
patternStr += '|${jumpUrlKeysList.map(RegExp.escape).join('|')}';
}
patternStr += '|${Constants.urlPattern}';
final RegExp pattern = RegExp(patternStr);
List<String> matchedStrs = [];
void addPlainTextSpan(str) {
spanChildren.add(TextSpan(
text: str,
));
// TextSpan(
//
// text: str,
// recognizer: TapGestureRecognizer()
// ..onTap = () => replyReply
// ?.call(replyItem.root == 0 ? replyItem : fReplyItem)))));
}
late final bool enableWordRe =
GStorage.setting.get(SettingBoxKey.enableWordRe, defaultValue: false);
// 分割文本并处理每个部分
message.splitMapJoin(
pattern,
onMatch: (Match match) {
String matchStr = match[0]!;
if (content.emote.containsKey(matchStr)) {
// 处理表情
final int size = content.emote[matchStr]!.size.toInt();
spanChildren.add(WidgetSpan(
child: ExcludeSemantics(
child: NetworkImgLayer(
src: content.emote[matchStr]?.hasGifUrl() == true
? content.emote[matchStr]?.gifUrl
: content.emote[matchStr]?.url,
type: 'emote',
width: size * 20,
height: size * 20,
semanticsLabel: matchStr,
)),
));
} else if (matchStr.startsWith("@") &&
content.atNameToMid.containsKey(matchStr.substring(1))) {
// 处理@用户
final String userName = matchStr.substring(1);
final int userId = content.atNameToMid[userName]!.toInt();
spanChildren.add(
TextSpan(
text: matchStr,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed('/member?mid=$userId');
},
),
);
} else if (RegExp(r'^\b(?:\d+[:])?\d+[:]\d+\b$').hasMatch(matchStr)) {
matchStr = matchStr.replaceAll('', ':');
bool isValid = false;
try {
List<int> split = matchStr
.split(':')
.map((item) => int.parse(item))
.toList()
.reversed
.toList();
int seek = 0;
for (int i = 0; i < split.length; i++) {
seek += split[i] * pow(60, i).toInt();
}
int duration = Get.find<VideoDetailController>(
tag: getTag?.call() ?? Get.arguments['heroTag'],
).data.timeLength ??
0;
isValid = seek * 1000 <= duration;
} catch (e) {
debugPrint('failed to validate: $e');
}
spanChildren.add(
TextSpan(
text: isValid ? ' $matchStr ' : matchStr,
style: isValid && isVideoPage
? TextStyle(
color: Theme.of(context).colorScheme.primary,
)
: null,
recognizer: isValid
? (TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
if (isVideoPage) {
try {
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
type: 'slider');
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
}
}
})
: null,
),
);
} else {
String appUrlSchema = '';
if (content.url[matchStr] != null &&
!matchedStrs.contains(matchStr)) {
appUrlSchema = content.url[matchStr]!.appUrlSchema;
if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
addPlainTextSpan(matchStr);
return "";
}
spanChildren.addAll(
[
if (content.url[matchStr]?.hasPrefixIcon() == true) ...[
WidgetSpan(
child: Image.network(
content.url[matchStr]!.prefixIcon.http2https,
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan(
text: content.url[matchStr]!.title,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
late final String title = content.url[matchStr]!.title;
if (appUrlSchema == '') {
if (RegExp(r'^(av|bv)', caseSensitive: false)
.hasMatch(matchStr)) {
UrlUtils.matchUrlPush(matchStr, '');
} else {
RegExpMatch? firstMatch = RegExp(
r'^cv(\d+)$|/read/cv(\d+)|note-app/view\?cvid=(\d+)',
caseSensitive: false,
).firstMatch(matchStr);
String? cvid = firstMatch?.group(1) ??
firstMatch?.group(2) ??
firstMatch?.group(3);
if (cvid != null) {
Get.toNamed('/htmlRender', parameters: {
'url': 'https://www.bilibili.com/read/cv$cvid',
'title': title,
'id': 'cv$cvid',
'dynamicType': 'read'
});
return;
}
Utils.handleWebview(matchStr);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult',
parameters: {'keyword': title});
} else {
Utils.handleWebview(matchStr);
}
}
},
)
],
);
// 只显示一次
matchedStrs.add(matchStr);
} else if (matchStr.length > 1 &&
content.topic[matchStr.substring(1, matchStr.length - 1)] !=
null) {
spanChildren.add(
TextSpan(
text: matchStr,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
final String topic =
matchStr.substring(1, matchStr.length - 1);
Get.toNamed('/searchResult',
parameters: {'keyword': topic});
},
),
);
} else if (RegExp(Constants.urlPattern).hasMatch(matchStr)) {
spanChildren.add(
TextSpan(
text: matchStr,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Utils.handleWebview(matchStr);
},
),
);
} else {
addPlainTextSpan(matchStr);
}
}
return '';
},
onNonMatch: (String nonMatchStr) {
addPlainTextSpan(nonMatchStr);
return nonMatchStr;
},
);
if (content.url.keys.isNotEmpty) {
List<String> unmatchedItems = content.url.keys
.toList()
.where((item) => !content.message.contains(item))
.toList();
if (unmatchedItems.isNotEmpty) {
for (int i = 0; i < unmatchedItems.length; i++) {
String patternStr = unmatchedItems[i];
if (content.url[patternStr]?.extra.isWordSearch == true &&
enableWordRe.not) {
continue;
}
spanChildren.addAll(
[
if (content.url[patternStr]?.hasPrefixIcon() == true) ...[
WidgetSpan(
child: Image.network(
content.url[patternStr]!.prefixIcon.http2https,
height: 19,
color: Theme.of(context).colorScheme.primary,
),
)
],
TextSpan(
text: content.url[patternStr]!.title,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String? cvid = RegExp(r'note-app/view\?cvid=(\d+)')
.firstMatch(patternStr)
?.group(1);
if (cvid != null) {
Get.toNamed('/htmlRender', parameters: {
'url': 'https://www.bilibili.com/read/cv$cvid',
'title': '',
'id': 'cv$cvid',
'dynamicType': 'read'
});
return;
}
Utils.handleWebview(patternStr);
},
)
],
);
}
}
}
if (didExceedMaxLines == true) {
spanChildren.add(
TextSpan(
text: '\n查看更多',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
);
}
// 图片渲染
if (content.pictures.isNotEmpty) {
spanChildren.add(const TextSpan(text: '\n'));
spanChildren.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, constraints) => imageview(
constraints.maxWidth,
content.pictures
.map(
(item) => ImageModel(
width: item.imgWidth,
height: item.imgHeight,
url: item.imgSrc,
),
)
.toList(),
onViewImage: onViewImage,
onDismissed: onDismissed,
callback: callback,
),
),
),
);
}
// 笔记链接
if (content.hasRichText()) {
spanChildren.add(
TextSpan(
text: ' 笔记',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => Utils.handleWebview(content.richText.note.clickUrl),
),
);
}
// spanChildren.add(TextSpan(text: matchMember));
return TextSpan(children: spanChildren);
}
Widget morePanel({
required BuildContext context,
required ReplyInfo item,
required onDelete,
required bool isSubReply,
}) {
int ownerMid = Accounts.main.mid;
Future<dynamic> menuActionHandler(String type) async {
late String message = item.content.message;
switch (type) {
case 'report':
Get.back();
autoWrapReportDialog(
context,
ReportOptions.commentReport,
(reasonType, reasonDesc, banUid) async {
final res = await Request().post(
'/x/v2/reply/report',
data: {
'add_blacklist': banUid,
'csrf': await Request.getCsrf(),
'gaia_source': 'main_h5',
'oid': item.oid,
'platform': 'android',
'reason': reasonType,
'rpid': item.id,
'scene': 'main',
'type': 1,
if (reasonType == 0) 'content': reasonDesc!
},
options:
Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
onDelete?.call(item.id.toInt());
}
return res.data as Map;
},
);
break;
case 'copyAll':
Get.back();
Utils.copyText(message);
break;
case 'copyFreedom':
Get.back();
showDialog(
context: context,
builder: (context) {
return Dialog(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: SelectableText(message),
),
);
},
);
break;
case 'delete':
Get.back();
bool? isDelete = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('删除评论'),
content: Text.rich(
TextSpan(
children: [
TextSpan(text: '确定删除这条评论吗?\n\n'),
if (ownerMid != item.member.mid.toInt()) ...[
TextSpan(
text: '@${item.member.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
TextSpan(text: ':\n'),
],
TextSpan(text: message),
],
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Get.back(result: false);
},
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back(result: true);
},
child: const Text('确定'),
),
],
);
},
);
if (isDelete == null || !isDelete) {
return;
}
SmartDialog.showLoading(msg: '删除中...');
var result = await VideoHttp.replyDel(
type: item.type.toInt(),
oid: item.oid.toInt(),
rpid: item.id.toInt(),
);
SmartDialog.dismiss();
if (result['status']) {
SmartDialog.showToast('删除成功');
onDelete?.call(item.id.toInt());
} else {
SmartDialog.showToast('删除失败, ${result["msg"]}');
}
break;
case 'checkReply':
Get.back();
onCheckReply?.call(item);
break;
case 'top':
Get.back();
onToggleTop?.call(item.replyControl.isUpTop, item.id.toInt());
break;
case 'saveReply':
Get.back();
Get.generalDialog(
barrierLabel: '',
barrierDismissible: true,
pageBuilder: (context, animation, secondaryAnimation) {
return ReplySavePanel(replyItem: item);
},
transitionDuration: const Duration(milliseconds: 255),
transitionBuilder: (context, animation, secondaryAnimation, child) {
var tween = Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: Curves.easeInOut));
return FadeTransition(
opacity: animation.drive(tween),
child: child,
);
},
routeSettings: RouteSettings(arguments: Get.arguments),
);
break;
default:
}
}
Color errorColor = Theme.of(context).colorScheme.error;
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: Get.back,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
if (ownerMid == upMid.toInt() ||
ownerMid == item.member.mid.toInt())
ListTile(
onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0,
leading:
Icon(Icons.delete_outlined, color: errorColor, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (ownerMid != 0)
ListTile(
onTap: () => menuActionHandler('report'),
minLeadingWidth: 0,
leading: Icon(Icons.error_outline, color: errorColor, size: 19),
title: Text('举报',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (replyLevel == '1' &&
isSubReply.not &&
ownerMid == upMid.toInt())
ListTile(
onTap: () => menuActionHandler('top'),
minLeadingWidth: 0,
leading: Icon(Icons.vertical_align_top, size: 19),
title: Text('${replyItem.replyControl.isUpTop ? '取消' : ''}置顶',
style: Theme.of(context).textTheme.titleSmall!),
),
ListTile(
onTap: () => menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title:
Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () => menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title:
Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () => menuActionHandler('saveReply'),
minLeadingWidth: 0,
leading: const Icon(Icons.save_alt, size: 19),
title:
Text('保存评论', style: Theme.of(context).textTheme.titleSmall),
),
if (item.mid.toInt() == ownerMid)
ListTile(
onTap: () => menuActionHandler('checkReply'),
minLeadingWidth: 0,
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined, size: 19),
const Icon(Icons.reply, size: 12),
],
),
title:
Text('检查评论', style: Theme.of(context).textTheme.titleSmall),
),
],
),
),
);
}
}