Merge pull request #470 from orz12/fix-reply-reply-parse2
fix: 评论区识别逻辑重构,修复含有关键词的评论重复出现的问题
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
@ -539,18 +540,6 @@ InlineSpan buildContent(
|
|||||||
// replyReply 查看二楼回复(回复详情)回调
|
// replyReply 查看二楼回复(回复详情)回调
|
||||||
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
|
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
|
||||||
final content = replyItem.content;
|
final content = replyItem.content;
|
||||||
if (content.emote.isEmpty &&
|
|
||||||
content.atNameToMid.isEmpty &&
|
|
||||||
content.jumpUrl.isEmpty &&
|
|
||||||
content.vote.isEmpty &&
|
|
||||||
content.pictures.isEmpty) {
|
|
||||||
return TextSpan(
|
|
||||||
text: content.message,
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap =
|
|
||||||
() => replyReply(replyItem.root == 0 ? replyItem : fReplyItem),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final List<InlineSpan> spanChilds = <InlineSpan>[];
|
final List<InlineSpan> spanChilds = <InlineSpan>[];
|
||||||
bool hasMatchMember = false;
|
bool hasMatchMember = false;
|
||||||
|
|
||||||
@ -582,258 +571,171 @@ InlineSpan buildContent(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
|
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
|
||||||
if (content.message.contains('&')) {
|
content.message = content.message.replaceAll('&', '&')
|
||||||
content.message = content.message.replaceAll('&', '&');
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll(''', "'")
|
||||||
|
.replaceAll(' ', ' ');
|
||||||
|
// print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
|
||||||
|
// 构建正则表达式
|
||||||
|
final List<String> specialTokens = [
|
||||||
|
...content.emote.keys,
|
||||||
|
...content.atNameToMid.keys.map((e) => '@$e'),
|
||||||
|
...content.jumpUrl.keys.map((e) =>
|
||||||
|
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
|
||||||
|
];
|
||||||
|
|
||||||
|
String patternStr =
|
||||||
|
specialTokens.map(RegExp.escape).join('|');
|
||||||
|
if (patternStr.isNotEmpty) {
|
||||||
|
patternStr += "|";
|
||||||
}
|
}
|
||||||
// 匹配表情
|
patternStr += r'(\b\d{1,2}[::]\d{2}\b)';
|
||||||
|
final RegExp pattern = RegExp(patternStr);
|
||||||
|
List<String> matchedStrs = [];
|
||||||
|
void addPlainTextSpan(str){
|
||||||
|
spanChilds.add(TextSpan(
|
||||||
|
text: str,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () =>
|
||||||
|
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
||||||
|
}
|
||||||
|
// 分割文本并处理每个部分
|
||||||
content.message.splitMapJoin(
|
content.message.splitMapJoin(
|
||||||
RegExp(r"\[.*?\]"),
|
pattern,
|
||||||
onMatch: (Match match) {
|
onMatch: (Match match) {
|
||||||
final String matchStr = match[0]!;
|
String matchStr = match[0]!;
|
||||||
if (content.emote.isNotEmpty &&
|
if (content.emote.containsKey(matchStr)) {
|
||||||
matchStr.indexOf('[') == matchStr.lastIndexOf('[') &&
|
// 处理表情
|
||||||
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) {
|
|
||||||
final int size = content.emote[matchStr]['meta']['size'];
|
final int size = content.emote[matchStr]['meta']['size'];
|
||||||
if (content.emote.keys.contains(matchStr)) {
|
spanChilds.add(WidgetSpan(
|
||||||
spanChilds.add(
|
child: NetworkImgLayer(
|
||||||
WidgetSpan(
|
src: content.emote[matchStr]['url'],
|
||||||
child: NetworkImgLayer(
|
type: 'emote',
|
||||||
src: content.emote[matchStr]['url'],
|
width: size * 20,
|
||||||
type: 'emote',
|
height: size * 20,
|
||||||
width: size * 20,
|
),
|
||||||
height: size * 20,
|
));
|
||||||
),
|
} else if (matchStr.startsWith("@") &&
|
||||||
),
|
content.atNameToMid.containsKey(matchStr.substring(1))) {
|
||||||
);
|
// 处理@用户
|
||||||
} else {
|
final String userName = matchStr.substring(1);
|
||||||
spanChilds.add(TextSpan(
|
final int userId = content.atNameToMid[userName];
|
||||||
text: matchStr,
|
spanChilds.add(
|
||||||
recognizer: TapGestureRecognizer()
|
TextSpan(
|
||||||
..onTap = () =>
|
|
||||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
|
||||||
return matchStr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
spanChilds.add(TextSpan(
|
|
||||||
text: matchStr,
|
text: matchStr,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () =>
|
..onTap = () {
|
||||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
final String heroTag = Utils.makeHeroTag(userId);
|
||||||
return matchStr;
|
Get.toNamed(
|
||||||
}
|
'/member?mid=$userId',
|
||||||
return '';
|
arguments: {'face': '', 'heroTag': heroTag},
|
||||||
},
|
|
||||||
onNonMatch: (String str) {
|
|
||||||
// 匹配@用户
|
|
||||||
String matchMember = str;
|
|
||||||
if (content.atNameToMid.isNotEmpty) {
|
|
||||||
final List atNameToMidKeys = content.atNameToMid.keys.toList();
|
|
||||||
RegExp reg = RegExp(atNameToMidKeys.map((key) => key).join('|'));
|
|
||||||
// if (!content.message.contains(':')) {
|
|
||||||
// reg = RegExp(r"@.*( |:)");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 只@用户没有内容
|
|
||||||
if (!content.message.contains(':') ||
|
|
||||||
(content.atNameToMid.length == 1 &&
|
|
||||||
content.message == '@${content.members.first.uname}')) {
|
|
||||||
reg = RegExp(r"@.*( |:|$)");
|
|
||||||
}
|
|
||||||
matchMember = str.splitMapJoin(
|
|
||||||
reg,
|
|
||||||
onMatch: (Match match) {
|
|
||||||
if (match[0] != null) {
|
|
||||||
hasMatchMember = true;
|
|
||||||
content.atNameToMid.forEach((key, value) {
|
|
||||||
if (str.contains('回复')) {
|
|
||||||
spanChilds.add(
|
|
||||||
TextSpan(
|
|
||||||
text: '回复 ',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize:
|
|
||||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
spanChilds.add(
|
|
||||||
TextSpan(
|
|
||||||
text: '@$key',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize:
|
|
||||||
Theme.of(context).textTheme.titleSmall!.fontSize,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () {
|
|
||||||
final String heroTag = Utils.makeHeroTag(value);
|
|
||||||
Get.toNamed(
|
|
||||||
'/member?mid=$value',
|
|
||||||
arguments: {'face': '', 'heroTag': heroTag},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
}
|
),
|
||||||
return '';
|
);
|
||||||
},
|
} else if (RegExp(r'^\b[0-9]{1,2}[::][0-9]{2}\b$').hasMatch(matchStr)) {
|
||||||
onNonMatch: (String str) {
|
spanChilds.add(
|
||||||
if (!str.contains('@')) {
|
TextSpan(
|
||||||
spanChilds.add(TextSpan(text: str));
|
text: ' $matchStr ',
|
||||||
}
|
style: TextStyle(
|
||||||
print(str);
|
color: Theme.of(context).colorScheme.primary,
|
||||||
return str;
|
),
|
||||||
},
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () {
|
||||||
|
// 跳转到指定位置
|
||||||
|
try {
|
||||||
|
matchStr = matchStr.replaceAll(':', ':');
|
||||||
|
SmartDialog.showToast('跳转至:$matchStr');
|
||||||
|
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
|
||||||
|
.plPlayerController
|
||||||
|
.seekTo(
|
||||||
|
Duration(seconds: Utils.duration(matchStr)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
SmartDialog.showToast('跳转失败: $e');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
matchMember = str;
|
// print("matchStr=$matchStr");
|
||||||
}
|
String appUrlSchema = '';
|
||||||
|
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
|
||||||
// 匹配 jumpUrl
|
defaultValue: false) as bool;
|
||||||
String matchUrl = matchMember;
|
if (content.jumpUrl[matchStr] != null &&
|
||||||
if (content.jumpUrl.isNotEmpty) {
|
!matchedStrs.contains(matchStr)) {
|
||||||
final List urlKeys = content.jumpUrl.keys.toList().reversed.toList();
|
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
||||||
for (int index = 0; index < urlKeys.length; index++) {
|
if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
|
||||||
var i = urlKeys[index];
|
addPlainTextSpan(matchStr);
|
||||||
if (i.contains('?')) {
|
return "";
|
||||||
urlKeys[index] = i.replaceAll('?', '\\?');
|
|
||||||
}
|
}
|
||||||
if (i.contains('+')) {
|
|
||||||
urlKeys[index] = i.replaceAll('+', '\\+');
|
|
||||||
}
|
|
||||||
if (i.contains('*')) {
|
|
||||||
urlKeys[index] = i.replaceAll('*', '\\*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (hasMatchMember) {
|
|
||||||
matchMember = matchMember.split('回复 @ :').length > 1
|
|
||||||
? matchMember.split('回复 @ :')[1]
|
|
||||||
: matchMember;
|
|
||||||
}
|
|
||||||
matchUrl = matchMember.splitMapJoin(
|
|
||||||
/// RegExp.escape() 转义特殊字符
|
|
||||||
RegExp(urlKeys.map((key) => key).join("|")),
|
|
||||||
// RegExp('What does the fox say\\?'),
|
|
||||||
onMatch: (Match match) {
|
|
||||||
final String matchStr = match[0]!;
|
|
||||||
String appUrlSchema = '';
|
|
||||||
if (content.jumpUrl[matchStr] != null) {
|
|
||||||
appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
|
|
||||||
}
|
|
||||||
// 默认不显示关键词
|
|
||||||
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
|
|
||||||
defaultValue: false) as bool;
|
|
||||||
if (content.jumpUrl[matchStr] != null) {
|
|
||||||
spanChilds.add(
|
|
||||||
TextSpan(
|
|
||||||
text: content.jumpUrl[matchStr]['title'],
|
|
||||||
style: TextStyle(
|
|
||||||
color: enableWordRe
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () {
|
|
||||||
if (appUrlSchema == '') {
|
|
||||||
final String str = Uri.parse(matchStr).pathSegments[0];
|
|
||||||
final Map matchRes = IdUtils.matchAvorBv(input: str);
|
|
||||||
final List matchKeys = matchRes.keys.toList();
|
|
||||||
if (matchKeys.isNotEmpty) {
|
|
||||||
if (matchKeys.first == 'BV') {
|
|
||||||
Get.toNamed(
|
|
||||||
'/searchResult',
|
|
||||||
parameters: {'keyword': matchRes['BV']},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Get.toNamed(
|
|
||||||
'/webview',
|
|
||||||
parameters: {
|
|
||||||
'url': matchStr,
|
|
||||||
'type': 'url',
|
|
||||||
'pageTitle': ''
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (appUrlSchema.startsWith('bilibili://search') &&
|
|
||||||
enableWordRe) {
|
|
||||||
Get.toNamed('/searchResult', parameters: {
|
|
||||||
'keyword': content.jumpUrl[matchStr]['title']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) {
|
|
||||||
spanChilds.add(
|
|
||||||
WidgetSpan(
|
|
||||||
child: Icon(
|
|
||||||
FontAwesomeIcons.magnifyingGlass,
|
|
||||||
size: 9,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
alignment: PlaceholderAlignment.top,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
onNonMatch: (String str) {
|
|
||||||
spanChilds.add(TextSpan(
|
|
||||||
text: str,
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => replyReply(
|
|
||||||
replyItem.root == 0 ? replyItem : fReplyItem)));
|
|
||||||
return str;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
str = matchUrl.splitMapJoin(
|
|
||||||
RegExp(r'\b\d{2}:\d{2}\b'),
|
|
||||||
onMatch: (Match match) {
|
|
||||||
String matchStr = match[0]!;
|
|
||||||
spanChilds.add(
|
spanChilds.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: ' $matchStr ',
|
text: content.jumpUrl[matchStr]['title'],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () {
|
..onTap = () {
|
||||||
// 跳转到指定位置
|
if (appUrlSchema == '') {
|
||||||
try {
|
final String str = Uri.parse(matchStr).pathSegments[0];
|
||||||
Get.find<VideoDetailController>(
|
final Map matchRes = IdUtils.matchAvorBv(input: str);
|
||||||
tag: Get.arguments['heroTag'])
|
final List matchKeys = matchRes.keys.toList();
|
||||||
.plPlayerController
|
if (matchKeys.isNotEmpty) {
|
||||||
.seekTo(
|
if (matchKeys.first == 'BV') {
|
||||||
Duration(seconds: Utils.duration(matchStr)),
|
Get.toNamed(
|
||||||
|
'/searchResult',
|
||||||
|
parameters: {'keyword': matchRes['BV']},
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
}
|
||||||
|
} else {
|
||||||
|
Get.toNamed(
|
||||||
|
'/webview',
|
||||||
|
parameters: {
|
||||||
|
'url': matchStr,
|
||||||
|
'type': 'url',
|
||||||
|
'pageTitle': ''
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (appUrlSchema.startsWith('bilibili://search')) {
|
||||||
|
Get.toNamed('/searchResult', parameters: {
|
||||||
|
'keyword': content.jumpUrl[matchStr]['title']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return '';
|
if (appUrlSchema.startsWith('bilibili://search')) {
|
||||||
},
|
spanChilds.add(
|
||||||
onNonMatch: (str) {
|
WidgetSpan(
|
||||||
return str;
|
child: Icon(
|
||||||
},
|
FontAwesomeIcons.magnifyingGlass,
|
||||||
);
|
size: 9,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) {
|
),
|
||||||
if (str != '') {
|
alignment: PlaceholderAlignment.top,
|
||||||
spanChilds.add(TextSpan(
|
),
|
||||||
text: str,
|
);
|
||||||
recognizer: TapGestureRecognizer()
|
}
|
||||||
..onTap = () =>
|
// 只显示一次
|
||||||
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
|
matchedStrs.add(matchStr);
|
||||||
|
} else {
|
||||||
|
addPlainTextSpan(matchStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str;
|
return '';
|
||||||
|
},
|
||||||
|
onNonMatch: (String nonMatchStr) {
|
||||||
|
addPlainTextSpan(nonMatchStr);
|
||||||
|
return nonMatchStr;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -841,10 +743,10 @@ InlineSpan buildContent(
|
|||||||
if (content.pictures.isNotEmpty) {
|
if (content.pictures.isNotEmpty) {
|
||||||
final List<String> picList = <String>[];
|
final List<String> picList = <String>[];
|
||||||
final int len = content.pictures.length;
|
final int len = content.pictures.length;
|
||||||
|
spanChilds.add(const TextSpan(text: '\n'));
|
||||||
if (len == 1) {
|
if (len == 1) {
|
||||||
Map pictureItem = content.pictures.first;
|
Map pictureItem = content.pictures.first;
|
||||||
picList.add(pictureItem['img_src']);
|
picList.add(pictureItem['img_src']);
|
||||||
spanChilds.add(const TextSpan(text: '\n'));
|
|
||||||
spanChilds.add(
|
spanChilds.add(
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
|
Reference in New Issue
Block a user