feat: vote pabel (#807)

This commit is contained in:
My-Responsitories
2025-05-04 13:53:00 +08:00
committed by GitHub
parent 9b3c3efb09
commit 2cfad80214
9 changed files with 755 additions and 300 deletions

View File

@@ -4,6 +4,7 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel;
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
import 'package:PiliPlus/models/dynamics/article_content_model.dart'
show ArticleContentModel, Style, Word;
import 'package:PiliPlus/models/dynamics/result.dart';
@@ -42,11 +43,11 @@ class OpusContent extends StatelessWidget {
fontSize: fontSize,
);
static TextSpan _getSpan(Word? word) => TextSpan(
static TextSpan _getSpan(Word? word, [Color? defaultColor]) => TextSpan(
text: word?.words,
style: _getStyle(
word?.style,
word?.color != null ? Color(word!.color!) : null,
word?.color != null ? Color(word!.color!) : defaultColor,
word?.fontSize,
));
@@ -64,8 +65,9 @@ class OpusContent extends StatelessWidget {
final element = opus[index];
try {
switch (element.paraType) {
case 1:
return SelectableText.rich(
case 1 || 4:
final isQuote = element.paraType == 4;
Widget widget = SelectableText.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text?.nodes?.map<TextSpan>((item) {
@@ -102,50 +104,26 @@ class OpusContent extends StatelessWidget {
],
);
}
return _getSpan(item.word);
return _getSpan(
item.word, isQuote ? colorScheme.onSurfaceVariant : null);
}).toList()),
);
case 4:
return Container(
padding:
const EdgeInsets.only(left: 8, top: 4, right: 4, bottom: 4),
decoration: BoxDecoration(
border: Border(
left:
BorderSide(color: colorScheme.outlineVariant, width: 4),
if (isQuote) {
widget = Container(
padding: const EdgeInsets.only(
left: 8, top: 4, right: 4, bottom: 4),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: colorScheme.outlineVariant, width: 4),
),
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: colorScheme.onInverseSurface,
),
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: colorScheme.onInverseSurface,
),
child: SelectableText.rich(
textAlign: element.align == 1 ? TextAlign.center : null,
TextSpan(
children: element.text?.nodes?.map<TextSpan>((item) {
if (item.rich != null) {
return TextSpan(
text: '\u{1F517}${item.rich?.text}',
style: _getStyle(item.rich?.style, colorScheme.primary),
recognizer: item.rich?.jumpUrl == null
? null
: (TapGestureRecognizer()
..onTap = () {
PiliScheme.routePushFromUrl(
item.rich!.jumpUrl!);
}),
);
}
return TextSpan(
text: item.word?.words,
style: _getStyle(
item.word?.style,
item.word?.color != null
? Color(item.word!.color!).withOpacity(0.7)
: colorScheme.onSurface.withOpacity(0.7),
item.word?.fontSize,
));
}).toList()),
),
);
child: widget,
);
}
return widget;
case 2 when (element.pic != null):
element.pic!.pics!.first.onCalHeight(maxWidth);
return Hero(
@@ -209,12 +187,10 @@ class OpusContent extends StatelessWidget {
try {
if (element.linkCard!.card!.type ==
'LINK_CARD_TYPE_VOTE') {
Get.toNamed(
'/webview',
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${element.linkCard!.card!.oid}',
},
showVoteDialog(
context,
element.linkCard!.card!.vote?.voteId ??
int.parse(element.linkCard!.card!.oid!),
);
return;
}

View File

@@ -1,7 +1,9 @@
import 'package:PiliPlus/common/widgets/image/image_view.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.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';
@@ -40,214 +42,208 @@ TextSpan? richNode(
return null;
} else {
for (var i in richTextNodes) {
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
spanChildren.add(
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)),
);
}
// @用户
else if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () => Get.toNamed('/member?mid=${i.rid}'),
child: Text(
' ${i.text}',
style: authorStyle,
switch (i.type) {
case 'RICH_TEXT_NODE_TYPE_TEXT':
spanChildren.add(
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)),
);
break;
// @用户
case 'RICH_TEXT_NODE_TYPE_AT':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () => Get.toNamed('/member?mid=${i.rid}'),
child: Text(
' ${i.text}',
style: authorStyle,
),
),
],
),
),
);
break;
// 话题
case 'RICH_TEXT_NODE_TYPE_TOPIC':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.origText}',
style: authorStyle,
),
],
),
),
);
}
// 话题
else if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.origText}',
style: authorStyle,
),
),
),
);
}
// 网页链接
else if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.link,
size: 20,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
String? url = i.origText;
if (url == null) {
SmartDialog.showToast('未获取到链接');
return;
}
PiliScheme.routePushFromUrl(url);
},
child: Text(
i.text ?? '',
style: authorStyle,
);
break;
// 网页链接
case 'RICH_TEXT_NODE_TYPE_WEB':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.link,
size: 20,
color: theme.colorScheme.primary,
),
),
),
);
}
// 投票
else if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
try {
String dynamicId = item.basic!.commentIdStr!;
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
String? url = i.origText;
if (url == null) {
SmartDialog.showToast('未获取到链接');
return;
}
PiliScheme.routePushFromUrl(url);
},
child: Text(
i.text ?? '',
style: authorStyle,
),
),
),
);
break;
// 投票
case 'RICH_TEXT_NODE_TYPE_VOTE':
spanChildren.add(
TextSpan(
text: '投票:${i.text}',
style: TextStyle(
color: theme.colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
final dynIdStr = item.basic?.commentIdStr;
final dynId =
dynIdStr != null ? int.tryParse(dynIdStr) : null;
showVoteDialog(context, int.parse(i.rid!), dynId);
},
),
);
break;
// 表情
case 'RICH_TEXT_NODE_TYPE_EMOJI' when (i.emoji != null):
spanChildren.add(
WidgetSpan(
child: NetworkImgLayer(
src: i.emoji!.webpUrl ?? i.emoji!.gifUrl ?? i.emoji!.iconUrl,
type: 'emote',
width: (i.emoji!.size ?? 1) * 20,
height: (i.emoji!.size ?? 1) * 20,
),
),
);
break;
// 抽奖
case 'RICH_TEXT_NODE_TYPE_LOTTERY':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.redeem_rounded,
size: 16,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
Get.toNamed(
'/webview',
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
'https://www.bilibili.com/h5/lottery/result?business_id=${item.idStr}'
},
);
} catch (_) {}
},
child: Text(
'投票:${i.text}',
style: authorStyle,
},
child: Text(
'${i.origText} ',
style: authorStyle,
),
),
),
),
);
}
// 表情
else if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI' && i.emoji != null) {
spanChildren.add(
WidgetSpan(
child: NetworkImgLayer(
src: i.emoji!.webpUrl ?? i.emoji!.gifUrl ?? i.emoji!.iconUrl,
type: 'emote',
width: (i.emoji!.size ?? 1) * 20,
height: (i.emoji!.size ?? 1) * 20,
),
),
);
}
// 抽奖
else if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.redeem_rounded,
size: 16,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/h5/lottery/result?business_id=${item.idStr}'
},
);
},
child: Text(
'${i.origText} ',
style: authorStyle,
),
),
),
);
}
);
break;
/// TODO 商品
else if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.shopping_bag_outlined,
size: 16,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.text} ',
style: authorStyle,
/// TODO 商品
case 'RICH_TEXT_NODE_TYPE_GOODS':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.shopping_bag_outlined,
size: 16,
color: theme.colorScheme.primary,
),
),
),
);
}
// 投稿
else if (i.type == 'RICH_TEXT_NODE_TYPE_BV') {
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.play_circle_outline_outlined,
size: 16,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () async {
try {
int cid = await SearchHttp.ab2c(bvid: i.rid);
PageUtils.toVideoPage(
'bvid=${i.rid}&cid=$cid',
arguments: {
'heroTag': Utils.makeHeroTag(i.rid),
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
child: Text(
'${i.text} ',
style: authorStyle,
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {},
child: Text(
'${i.text} ',
style: authorStyle,
),
),
),
),
);
} else if (i.type == 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') {
if (i.pics?.isNotEmpty == true) {
);
break;
// 投稿
case 'RICH_TEXT_NODE_TYPE_BV':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.play_circle_outline_outlined,
size: 16,
color: theme.colorScheme.primary,
),
),
);
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () async {
try {
int cid = await SearchHttp.ab2c(bvid: i.rid);
PageUtils.toVideoPage(
'bvid=${i.rid}&cid=$cid',
arguments: {
'heroTag': Utils.makeHeroTag(i.rid),
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}
},
child: Text(
'${i.text} ',
style: authorStyle,
),
),
),
);
break;
case 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE'
when (i.pics?.isNotEmpty == true):
spanChildren.add(TextSpan(text: '\n'));
spanChildren.add(
WidgetSpan(
@@ -267,17 +263,17 @@ TextSpan? richNode(
),
),
);
} else {
break;
default:
spanChildren.add(
TextSpan(
text: '${i.text}',
style: authorStyle,
),
);
}
break;
}
}
return TextSpan(children: spanChildren);
}
} catch (err) {

View File

@@ -0,0 +1,388 @@
import 'dart:async';
import 'package:PiliPlus/common/widgets/dialog/report.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/dynamics/vote_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class VotePanel extends StatefulWidget {
final VoteInfo voteInfo;
final FutureOr<LoadingState<VoteInfo>> Function(Set<int>, bool) callback;
final bool embedded;
const VotePanel({
super.key,
required this.voteInfo,
required this.callback,
this.embedded = false,
});
@override
State<VotePanel> createState() => _VotePanelState();
}
class _VotePanelState extends State<VotePanel> {
bool anonymity = false;
late VoteInfo _voteInfo;
late final bool _embedded;
late final groupValue = _voteInfo.myVotes?.toSet() ?? {};
late var _percentage = _cnt2Percentage(_voteInfo.options);
late bool _enabled = groupValue.isEmpty &&
_voteInfo.endTime! * 1000 > DateTime.now().millisecondsSinceEpoch;
late bool _showPercentage = !_enabled;
late final _maxCnt = _voteInfo.choiceCnt ?? _voteInfo.options.length;
late final _selectedNum = ValueNotifier(groupValue.length);
late final _canVote = ValueNotifier(false);
@override
void initState() {
super.initState();
_voteInfo = widget.voteInfo;
_embedded = widget.embedded || widget.voteInfo.options.length < 5;
}
@override
void dispose() {
_selectedNum.dispose();
_canVote.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_voteInfo.title != null)
Text(_voteInfo.title!, style: theme.textTheme.titleMedium),
if (_voteInfo.desc != null)
Text(_voteInfo.desc!, style: theme.textTheme.titleSmall),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${DateTime.fromMillisecondsSinceEpoch(_voteInfo.endTime! * 1000).toString().substring(0, 19)}',
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: _voteInfo.joinNum.toString(),
style: TextStyle(color: theme.colorScheme.primary),
),
TextSpan(text: '人参与'),
],
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_enabled
? '投票选项'
: groupValue.isEmpty
? '已结束'
: '已完成',
),
ValueListenableBuilder(
valueListenable: _selectedNum,
builder: (_, val, __) => Text('$val / $_maxCnt'),
),
],
),
if (_embedded)
_buildContext()
else
Flexible(fit: FlexFit.loose, child: _buildContext()),
if (_enabled)
Padding(
padding: EdgeInsets.only(top: 16),
child: ValueListenableBuilder(
valueListenable: _canVote,
builder: (_, val, __) => OutlinedButton(
onPressed: val
? () async {
final res = await widget.callback(
groupValue,
anonymity,
);
if (res.isSuccess) {
if (mounted) {
setState(() {
_enabled = false;
_showPercentage = true;
_voteInfo = res.data;
_percentage = _cnt2Percentage(_voteInfo.options);
});
}
} else {
SmartDialog.showToast((res as Error).errMsg);
}
}
: null,
child: const Center(child: Text('投票')),
),
),
),
],
);
}
List<Widget> get _checkBoxs => [
CheckBoxText(
text: '显示比例',
selected: _showPercentage,
onChanged: (value) {
setState(() {
_showPercentage = value;
});
},
),
CheckBoxText(
text: '匿名',
selected: anonymity,
onChanged: (val) {
anonymity = val;
},
),
// TODO 转发到动态
];
Widget _buildOptions(int index) {
final opt = _voteInfo.options[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: PercentageChip<int>(
label: opt.optdesc!,
value: opt.optidx!,
groupValue: groupValue,
disabled: groupValue.length >= _maxCnt,
percentage: _showPercentage ? _percentage[index] : null,
callback: _enabled ? _toggleSelection : null,
));
}
Widget _buildContext() {
return _embedded
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...List.generate(_voteInfo.options.length, _buildOptions),
if (_enabled) ..._checkBoxs,
],
)
: CustomScrollView(
slivers: [
SliverList.builder(
itemCount: _voteInfo.options.length,
itemBuilder: (context, index) => _buildOptions(index),
),
if (_enabled) SliverList.list(children: _checkBoxs)
],
);
}
static List<double> _cnt2Percentage(List<Option> options) {
final total = options.fold(0, (sum, opt) => sum + opt.cnt);
return total == 0
? List<double>.filled(options.length, 0)
: options.map((i) => i.cnt / total).toList(growable: false);
}
bool _toggleSelection(bool val) {
_selectedNum.value = groupValue.length;
_canVote.value = groupValue.isNotEmpty;
if (groupValue.length >= _maxCnt ||
(!val && groupValue.length + 1 == _maxCnt)) {
setState(() {});
return true;
}
return false;
}
}
class PercentageChip<T> extends StatefulWidget {
final String label;
final T value;
final Set<T> groupValue;
final double? percentage;
final bool disabled;
final bool? Function(bool)? callback;
const PercentageChip({
super.key,
required this.label,
required this.value,
required this.groupValue,
this.disabled = false,
this.percentage,
this.callback,
});
@override
State<PercentageChip<T>> createState() => _PercentageChipState<T>();
}
class _PercentageChipState<T> extends State<PercentageChip<T>> {
late Set<T> groupValue;
@override
void initState() {
super.initState();
groupValue = widget.groupValue;
}
bool get selected => groupValue.contains(widget.value);
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return ChoiceChip(
labelPadding: EdgeInsets.zero,
padding: EdgeInsets.zero,
showCheckmark: false,
clipBehavior: Clip.antiAlias,
label: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
if (widget.percentage != null)
Positioned.fill(
left: 0,
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: widget.percentage,
child: ColoredBox(
color: selected
? colorScheme.inversePrimary
: colorScheme.outlineVariant,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.label),
if (selected)
Padding(
padding: const EdgeInsets.only(left: 4),
child: Icon(
Icons.check_circle,
size: 12,
color: colorScheme.onPrimaryContainer,
),
),
],
),
if (widget.percentage != null)
Text('${(widget.percentage! * 100).toStringAsFixed(0)}%'),
],
),
),
],
),
selected: selected,
onSelected: widget.disabled && (!selected || widget.callback == null)
? null
: (value) {
value
? groupValue.add(widget.value)
: groupValue.remove(widget.value);
if (widget.callback?.call(value) == true) setState(() {});
},
);
}
}
// class VoteCard extends StatefulWidget {
// final int voteId;
// final bool isSliver;
// const VoteCard(this.voteId, {super.key, this.isSliver = false});
// @override
// State<VoteCard> createState() => _VoteCardState();
// }
// class _VoteCardState extends State<VoteCard> {
// late Future<LoadingState<VoteInfo>> _futureBuilderFuture;
// @override
// void initState() {
// super.initState();
// _futureBuilderFuture = getInfo();
// }
// Future<LoadingState<VoteInfo>> getInfo() =>
// DynamicsHttp.voteInfo(widget.voteId);
// @override
// Widget build(BuildContext context) {
// return FutureBuilder(
// future: _futureBuilderFuture,
// builder: (context, snapshot) => switch (snapshot.data) {
// Success(response: final res) => VotePanel(
// embedded: true,
// voteInfo: res,
// callback: (votes, anonymity) => DynamicsHttp.doVote(
// voteId: widget.voteId,
// votes: votes.toList(),
// anonymity: anonymity,
// )),
// Error(errMsg: final msg) => HttpError(
// isSliver: widget.isSliver,
// errMsg: msg,
// onReload: () {
// setState(() {
// _futureBuilderFuture = getInfo();
// });
// }),
// _ => const SizedBox.shrink()
// });
// }
// }
Future showVoteDialog(BuildContext context, int voteId,
[int? dynamicId]) async {
final voteInfo = await DynamicsHttp.voteInfo(voteId);
if (context.mounted) {
if (voteInfo.isSuccess) {
await showDialog(
context: context,
builder: (context) => AlertDialog(
content: SizedBox(
width: 120,
child: VotePanel(
voteInfo: voteInfo.data,
callback: (votes, anonymity) => DynamicsHttp.doVote(
voteId: voteId,
votes: votes.toList(),
anonymity: anonymity,
dynamicId: dynamicId,
),
),
),
));
} else {
SmartDialog.showToast((voteInfo as Error).errMsg);
}
}
}

View File

@@ -8,6 +8,7 @@ import 'package:PiliPlus/common/widgets/dialog/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/pages/dynamics/widgets/vote.dart';
import 'package:PiliPlus/pages/save_panel/view.dart';
import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/reply/widgets/zan_grpc.dart';
@@ -59,6 +60,9 @@ class ReplyItemGrpc extends StatelessWidget {
final ValueChanged<ReplyInfo>? onCheckReply;
final Function(bool isUpTop, int rpid)? onToggleTop;
static final _voteRegExp = RegExp(r"\{vote:\d+?\}");
static final _timeRegExp = RegExp(r'^\b(?:\d+[:])?\d+[:]\d+\b$');
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
@@ -593,7 +597,7 @@ class ReplyItemGrpc extends StatelessWidget {
// 投票
if (content.hasVote()) {
message.splitMapJoin(RegExp(r"\{vote:\d+?\}"), onMatch: (Match match) {
message = message.replaceAllMapped(_voteRegExp, (Match match) {
spanChildren.add(
TextSpan(
text: '投票: ${content.vote.title}',
@@ -601,22 +605,11 @@ class ReplyItemGrpc extends StatelessWidget {
color: theme.colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/webview',
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${content.vote.id}',
},
);
},
..onTap = () => showVoteDialog(context, content.vote.id.toInt()),
),
);
return '';
}, onNonMatch: (String str) {
return str;
});
message = message.replaceAll(RegExp(r"\{vote:\d+?\}"), "");
}
// 构建正则表达式
final List<String> specialTokens = [
@@ -685,7 +678,7 @@ class ReplyItemGrpc extends StatelessWidget {
},
),
);
} else if (RegExp(r'^\b(?:\d+[:])?\d+[:]\d+\b$').hasMatch(matchStr)) {
} else if (_timeRegExp.hasMatch(matchStr)) {
matchStr = matchStr.replaceAll('', ':');
bool isValid = false;
try {