mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: vote pabel (#807)
This commit is contained in:
committed by
GitHub
parent
9b3c3efb09
commit
2cfad80214
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
63
lib/models/dynamics/vote_model.dart
Normal file
63
lib/models/dynamics/vote_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
388
lib/pages/dynamics/widgets/vote.dart
Normal file
388
lib/pages/dynamics/widgets/vote.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user