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

@@ -61,7 +61,11 @@ void autoWrapReportDialog(
),
),
),
BanUserCheckbox(onChanged: (value) => banUid = value),
Padding(
padding: const EdgeInsets.only(left: 14, top: 6),
child: CheckBoxText(
text: '拉黑该用户', onChanged: (value) => banUid = value),
),
],
),
),
@@ -145,44 +149,58 @@ class _ReasonFieldState extends State<ReasonField> {
}
}
class BanUserCheckbox extends StatefulWidget {
class CheckBoxText extends StatefulWidget {
final String text;
final ValueChanged<bool> onChanged;
final bool selected;
const BanUserCheckbox({super.key, required this.onChanged});
const CheckBoxText({
super.key,
required this.text,
required this.onChanged,
this.selected = false,
});
@override
State<BanUserCheckbox> createState() => _BanUserCheckboxState();
State<CheckBoxText> createState() => _CheckBoxTextState();
}
class _BanUserCheckboxState extends State<BanUserCheckbox> {
bool _banUid = false;
class _CheckBoxTextState extends State<CheckBoxText> {
late bool _selected;
@override
void initState() {
super.initState();
_selected = widget.selected;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: () {
setState(() => _banUid = !_banUid);
widget.onChanged(_banUid);
setState(() {
_selected = !_selected;
});
widget.onChanged(_selected);
},
child: Padding(
padding: const EdgeInsets.only(left: 18, top: 10),
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
size: 22,
_banUid
_selected
? Icons.check_box_outlined
: Icons.check_box_outline_blank,
color: _banUid
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant,
color: _selected
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
Text(
' 拉黑该用户',
style: TextStyle(
color: _banUid ? theme.colorScheme.primary : null,
),
' ${widget.text}',
style: TextStyle(color: _selected ? colorScheme.primary : null),
),
],
),

View File

@@ -795,4 +795,8 @@ class Api {
static const String gaiaVgateRegister = '/x/gaia-vgate/v1/register';
static const String gaiaVgateValidate = '/x/gaia-vgate/v1/validate';
static const String voteInfo = '/x/vote/vote_info';
static const String doVote = '/x/vote/do_vote';
}

View File

@@ -4,6 +4,7 @@ import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/dynamics/up.dart';
import 'package:PiliPlus/models/dynamics/vote_model.dart';
import 'package:PiliPlus/models/space_article/item.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/storage.dart';
@@ -219,4 +220,40 @@ class DynamicsHttp {
? LoadingState.success(DynamicItemModel.fromOpusJson(res.data['data']))
: LoadingState.error(res.data['message']);
}
static Future<LoadingState<VoteInfo>> voteInfo(dynamic voteId) async {
final res =
await Request().get(Api.voteInfo, queryParameters: {'vote_id': voteId});
return res.data['code'] == 0
? LoadingState.success(VoteInfo.fromSeparatedJson(res.data['data']))
: LoadingState.error(res.data['message']);
}
static Future<LoadingState<VoteInfo>> doVote({
required int voteId,
required List<int> votes,
bool anonymity = false,
int? dynamicId,
}) async {
final csrf = Accounts.main.csrf;
final data = {
'vote_id': 15141778,
'votes': votes,
'voter_uid': Accounts.main.mid,
'status': anonymity ? 1 : 0,
'op_bit': 0,
'dynamic_id': dynamicId ?? 0,
'csrf_token': csrf,
'csrf': csrf
};
final res = await Request().post(Api.doVote,
queryParameters: {'csrf': csrf},
data: data,
options: Options(contentType: Headers.jsonContentType));
return res.data['code'] == 0
? LoadingState.success(VoteInfo.fromJson(res.data['data']['vote_info']))
: LoadingState.error(res.data['message']);
}
}

View File

@@ -1,3 +1,5 @@
import 'package:PiliPlus/models/dynamics/vote_model.dart';
class ArticleContentModel {
int? align;
int? paraType;
@@ -188,7 +190,7 @@ class Card {
Common? common;
Live? live;
Opus? opus;
Vote? vote;
SimpleVoteInfo? vote;
Music? music;
Goods? goods;
@@ -201,7 +203,7 @@ class Card {
common = json['common'] == null ? null : Common.fromJson(json['common']);
live = json['live'] == null ? null : Live.fromJson(json['live']);
opus = json['opus'] == null ? null : Opus.fromJson(json['opus']);
vote = json['vote'] == null ? null : Vote.fromJson(json['vote']);
vote = json['vote'] == null ? null : SimpleVoteInfo.fromJson(json['vote']);
music = json['music'] == null ? null : Music.fromJson(json['music']);
goods = json['goods'] == null ? null : Goods.fromJson(json['goods']);
}
@@ -259,28 +261,6 @@ class Music {
}
}
class Vote {
int? choiceCnt;
int? defaultShare;
String? desc;
int? endTime;
int? status;
int? uid;
int? voteId;
late int joinNum;
Vote.fromJson(Map<String, dynamic> json) {
choiceCnt = json['choice_cnt'];
defaultShare = json['default_share'];
desc = json['desc'];
endTime = json['end_time'];
status = json['status'];
uid = json['uid'];
voteId = json['vote_id'];
joinNum = json['join_num'] ?? 0;
}
}
class Opus {
int? authorMid;
String? authorName;

View File

@@ -0,0 +1,63 @@
class SimpleVoteInfo {
int? choiceCnt;
int? defaultShare;
String? desc;
int? endTime;
int? status;
int? uid;
int? voteId;
late int joinNum;
SimpleVoteInfo.fromJson(Map<String, dynamic> json) {
choiceCnt = json['choice_cnt'];
defaultShare = json['default_share'];
desc = json['desc'];
endTime = json['end_time'];
status = json['status'];
uid = json['uid'];
voteId = json['vote_id'];
joinNum = json['join_num'] ?? 0;
}
}
class VoteInfo extends SimpleVoteInfo {
String? title;
int? ctime;
List<int>? myVotes;
late List<Option> options;
int? optionsCnt;
int? voterLevel;
String? face;
String? name;
VoteInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {
title = json['title'];
uid = json['vote_publisher'];
ctime = json['ctime'];
myVotes = (json['my_votes'] as List?)?.cast(); // doVote
options =
(json['options'] as List?)?.map((v) => Option.fromJson(v)).toList() ??
[];
optionsCnt = json['options_cnt'];
voterLevel = json['voter_level'];
face = json['face'];
name = json['name'];
}
factory VoteInfo.fromSeparatedJson(Map<String, dynamic> json) {
return VoteInfo.fromJson(json['vote_info'])
..myVotes = (json['my_votes'] as List?)?.cast(); // voteInfo
}
}
class Option {
int? optidx;
String? optdesc;
late int cnt;
Option.fromJson(Map<String, dynamic> json) {
optidx = json['opt_idx'];
optdesc = json['opt_desc'];
cnt = json['cnt'] ?? 0;
}
}

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),
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),
left: BorderSide(
color: colorScheme.outlineVariant, width: 4),
),
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!);
}),
child: widget,
);
}
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()),
),
);
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,13 +42,14 @@ TextSpan? richNode(
return null;
} else {
for (var i in richTextNodes) {
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
switch (i.type) {
case 'RICH_TEXT_NODE_TYPE_TEXT':
spanChildren.add(
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)),
);
}
break;
// @用户
else if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
case 'RICH_TEXT_NODE_TYPE_AT':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -64,9 +67,9 @@ TextSpan? richNode(
),
),
);
}
break;
// 话题
else if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
case 'RICH_TEXT_NODE_TYPE_TOPIC':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -79,9 +82,9 @@ TextSpan? richNode(
),
),
);
}
break;
// 网页链接
else if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
case 'RICH_TEXT_NODE_TYPE_WEB':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -111,35 +114,27 @@ TextSpan? richNode(
),
),
);
}
break;
// 投票
else if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
case 'RICH_TEXT_NODE_TYPE_VOTE':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
onTap: () {
try {
String dynamicId = item.basic!.commentIdStr!;
Get.toNamed(
'/webview',
parameters: {
'url':
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
},
);
} catch (_) {}
},
child: Text(
'投票:${i.text}',
style: authorStyle,
),
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;
// 表情
else if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI' && i.emoji != null) {
case 'RICH_TEXT_NODE_TYPE_EMOJI' when (i.emoji != null):
spanChildren.add(
WidgetSpan(
child: NetworkImgLayer(
@@ -150,9 +145,9 @@ TextSpan? richNode(
),
),
);
}
break;
// 抽奖
else if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
case 'RICH_TEXT_NODE_TYPE_LOTTERY':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -183,10 +178,10 @@ TextSpan? richNode(
),
),
);
}
break;
/// TODO 商品
else if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
case 'RICH_TEXT_NODE_TYPE_GOODS':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -209,9 +204,9 @@ TextSpan? richNode(
),
),
);
}
break;
// 投稿
else if (i.type == 'RICH_TEXT_NODE_TYPE_BV') {
case 'RICH_TEXT_NODE_TYPE_BV':
spanChildren.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@@ -246,8 +241,9 @@ TextSpan? richNode(
),
),
);
} else if (i.type == 'RICH_TEXT_NODE_TYPE_VIEW_PICTURE') {
if (i.pics?.isNotEmpty == true) {
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 {