feat: reply vote

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-04 18:04:46 +08:00
parent fd017e0581
commit 0036b58aab
10 changed files with 319 additions and 192 deletions

View File

@@ -2,10 +2,12 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart';
import 'package:PiliPlus/grpc/reply.dart'; import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/pages/video/reply/vote/reply_vote_mixin.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
abstract class CommonDynController extends ReplyController<MainListReply> { abstract class CommonDynController extends ReplyController<MainListReply>
with ReplyVoteMixin {
int get oid; int get oid;
int get replyType; int get replyType;

View File

@@ -10,6 +10,7 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/enum_with_label.dart'; import 'package:PiliPlus/models/common/enum_with_label.dart';
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/common/fab_mixin.dart';
import 'package:PiliPlus/pages/video/reply/vote/reply_vote_item.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart';
@@ -120,62 +121,78 @@ mixin CommonDynPageMixin<T extends StatefulWidget>
} }
Widget replyList(LoadingState<List<ReplyInfo>?> loadingState) { Widget replyList(LoadingState<List<ReplyInfo>?> loadingState) {
return switch (loadingState) { switch (loadingState) {
Loading() => SliverList.builder( case Loading():
itemCount: 12, return SliverList.builder(
itemBuilder: (context, index) => const VideoReplySkeleton(), itemCount: 12,
), itemBuilder: (context, index) => const VideoReplySkeleton(),
Success(:final response) => );
response != null && response.isNotEmpty case Success(:final response):
? SliverList.builder( if (response != null && response.isNotEmpty) {
itemCount: response.length + 1, var count = response.length + 1;
itemBuilder: (context, index) { final voteCard = controller.voteCard;
if (index == response.length) { final hasVote = voteCard != null;
controller.onLoadMore(); if (hasVote) {
return Container( count++;
alignment: Alignment.center, }
margin: EdgeInsets.only(bottom: padding.bottom), return SliverList.builder(
height: 125, itemCount: count,
child: Text( itemBuilder: (context, index) {
controller.isEnd ? '没有更多了' : '加载中...', if (hasVote) {
style: TextStyle( if (index == 0) {
fontSize: 12, return buildVoteCard(context, theme.colorScheme, voteCard);
color: theme.colorScheme.outline, } else {
), index--;
), }
); }
} else { if (index == response.length) {
return ReplyItemGrpc( controller.onLoadMore();
replyItem: response[index], return Container(
replyLevel: 1, alignment: Alignment.center,
replyReply: (replyItem, id) => margin: EdgeInsets.only(bottom: padding.bottom),
replyReply(context, replyItem, id), height: 125,
onReply: controller.onReply, child: Text(
onDelete: (item, subIndex) => controller.isEnd ? '没有更多了' : '加载中...',
controller.onRemove(index, item, subIndex), style: TextStyle(
upMid: controller.upMid, fontSize: 12,
onViewImage: hideFab, color: theme.colorScheme.outline,
onCheckReply: (item) => ),
controller.onCheckReply(item, isManual: true), ),
onToggleTop: (item) => controller.onToggleTop( );
item, } else {
index, return ReplyItemGrpc(
controller.oid, replyItem: response[index],
controller.replyType, replyLevel: 1,
), replyReply: (replyItem, id) =>
); replyReply(context, replyItem, id),
} onReply: controller.onReply,
}, onDelete: (item, subIndex) =>
) controller.onRemove(index, item, subIndex),
: HttpError( upMid: controller.upMid,
errMsg: '还没有评论', onViewImage: hideFab,
onReload: controller.onReload, onCheckReply: (item) =>
), controller.onCheckReply(item, isManual: true),
Error(:final errMsg) => HttpError( onToggleTop: (item) => controller.onToggleTop(
errMsg: errMsg, item,
onReload: controller.onReload, index,
), controller.oid,
}; controller.replyType,
),
);
}
},
);
}
return HttpError(
errMsg: '还没有评论',
onReload: controller.onReload,
);
case Error(:final errMsg):
return HttpError(
errMsg: errMsg,
onReload: controller.onReload,
);
}
} }
void replyReply(BuildContext context, ReplyInfo replyItem, int? id) { void replyReply(BuildContext context, ReplyInfo replyItem, int? id) {

View File

@@ -57,16 +57,15 @@ abstract class ReplyController<R> extends CommonListController<R, ReplyInfo> {
} }
@override @override
bool customHandleResponse(bool isRefresh, Success response) { bool customHandleResponse(bool isRefresh, Success<R> response) {
MainListReply data = response.response; final data = response.response as MainListReply;
cursorNext = data.cursor.next; cursorNext = data.cursor.next;
paginationReply = data.paginationReply; paginationReply = data.paginationReply;
count.value = data.subjectControl.count.toInt(); count.value = data.subjectControl.count.toInt();
if (isRefresh) { if (isRefresh) {
subjectControl = data.subjectControl; subjectControl = data.subjectControl;
upMid ??= data.subjectControl.upMid; upMid ??= data.subjectControl.upMid;
hasUpTop = data.hasUpTop(); if (hasUpTop = data.hasUpTop()) {
if (data.hasUpTop()) {
data.replies.insert(0, data.upTop); data.replies.insert(0, data.upTop);
} }
if (subjectControl?.title == ReplySortType.select.title) { if (subjectControl?.title == ReplySortType.select.title) {

View File

@@ -566,9 +566,7 @@ class _DynamicDetailPageState
color: theme.colorScheme.surface, color: theme.colorScheme.surface,
border: Border( border: Border(
top: BorderSide( top: BorderSide(
color: theme.colorScheme.outline.withValues( color: theme.colorScheme.outline.withValues(alpha: 0.08),
alpha: 0.08,
),
), ),
), ),
), ),

View File

@@ -5,10 +5,12 @@ import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/common/video/video_type.dart';
import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart';
import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/reply/vote/reply_vote_mixin.dart';
import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class VideoReplyController extends ReplyController<MainListReply> { class VideoReplyController extends ReplyController<MainListReply>
with ReplyVoteMixin {
VideoReplyController({ VideoReplyController({
required this.aid, required this.aid,
required this.videoType, required this.videoType,

View File

@@ -8,6 +8,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/common/fab_mixin.dart';
import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/pages/video/reply/controller.dart';
import 'package:PiliPlus/pages/video/reply/vote/reply_vote_item.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/pages/video/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/reply_reply/view.dart';
import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/feed_back.dart';
@@ -125,9 +126,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
}), }),
), ),
), ),
Obx( Obx(() => _buildBody(_videoReplyController.loadingState.value)),
() => _buildBody(_videoReplyController.loadingState.value),
),
], ],
), ),
Positioned( Positioned(
@@ -170,62 +169,75 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
} }
Widget _buildBody(LoadingState<List<ReplyInfo>?> loadingState) { Widget _buildBody(LoadingState<List<ReplyInfo>?> loadingState) {
return switch (loadingState) { switch (loadingState) {
Loading() => SliverList.builder( case Loading():
itemBuilder: (context, index) => const VideoReplySkeleton(), return SliverList.builder(
itemCount: 5, itemBuilder: (context, index) => const VideoReplySkeleton(),
), itemCount: 5,
Success(:final response) => );
response != null && response.isNotEmpty case Success(:final response):
? SliverList.builder( if (response != null && response.isNotEmpty) {
itemBuilder: (context, index) { var count = response.length + 1;
if (index == response.length) { final voteCard = _videoReplyController.voteCard;
_videoReplyController.onLoadMore(); final hasVote = voteCard != null;
return Container( if (hasVote) {
height: 125, count++;
alignment: .center, }
margin: .only(bottom: bottom), return SliverList.builder(
child: Text( itemBuilder: (context, index) {
_videoReplyController.isEnd ? '没有更多了' : '加载中...', if (hasVote) {
textAlign: .center, if (index == 0) {
style: TextStyle( return buildVoteCard(context, colorScheme, voteCard);
fontSize: 12, } else {
color: colorScheme.outline, index--;
), }
), }
); if (index == response.length) {
} else { _videoReplyController.onLoadMore();
return ReplyItemGrpc( return Container(
replyItem: response[index], height: 125,
replyLevel: widget.replyLevel, alignment: .center,
replyReply: replyReply, margin: .only(bottom: bottom),
onReply: _videoReplyController.onReply, child: Text(
onDelete: (item, subIndex) => _videoReplyController.isEnd ? '没有更多了' : '加载中...',
_videoReplyController.onRemove(index, item, subIndex), textAlign: .center,
upMid: _videoReplyController.upMid, style: TextStyle(fontSize: 12, color: colorScheme.outline),
getTag: () => heroTag, ),
onCheckReply: (item) => _videoReplyController );
.onCheckReply(item, isManual: true), } else {
onToggleTop: (item) => _videoReplyController.onToggleTop( return ReplyItemGrpc(
item, replyItem: response[index],
index, replyLevel: widget.replyLevel,
_videoReplyController.aid, replyReply: replyReply,
_videoReplyController.videoType.replyType, onReply: _videoReplyController.onReply,
), onDelete: (item, subIndex) =>
); _videoReplyController.onRemove(index, item, subIndex),
} upMid: _videoReplyController.upMid,
}, getTag: () => heroTag,
itemCount: response.length + 1, onCheckReply: (item) =>
) _videoReplyController.onCheckReply(item, isManual: true),
: HttpError( onToggleTop: (item) => _videoReplyController.onToggleTop(
errMsg: '还没有评论', item,
onReload: _videoReplyController.onReload, index,
), _videoReplyController.aid,
Error(:final errMsg) => HttpError( _videoReplyController.videoType.replyType,
errMsg: errMsg, ),
onReload: _videoReplyController.onReload, );
), }
}; },
itemCount: count,
);
}
return HttpError(
errMsg: '还没有评论',
onReload: _videoReplyController.onReload,
);
case Error(:final errMsg):
return HttpError(
errMsg: errMsg,
onReload: _videoReplyController.onReload,
);
}
} }
// 展示二级回复 // 展示二级回复

View File

@@ -0,0 +1,49 @@
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show VoteCard;
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
import 'package:PiliPlus/utils/num_utils.dart';
import 'package:flutter/material.dart';
Widget buildVoteCard(
BuildContext context,
ColorScheme colorScheme,
VoteCard voteCard,
) {
return InkWell(
onTap: () => showVoteDialog(context, voteCard.voteId.toInt()),
child: Padding(
padding: const .symmetric(horizontal: 12, vertical: 6),
child: Row(
spacing: 10,
children: [
Container(
decoration: BoxDecoration(
color: colorScheme.onInverseSurface,
borderRadius: const .all(.circular(8)),
),
width: 60,
height: 42,
child: Icon(
Icons.bar_chart_rounded,
color: colorScheme.onSurfaceVariant,
),
),
Expanded(
child: Column(
crossAxisAlignment: .start,
children: [
Text(voteCard.title, maxLines: 1, overflow: .ellipsis),
Text(
'${NumUtils.numFormat(voteCard.count.toInt())}人参与',
maxLines: 1,
overflow: .ellipsis,
style: TextStyle(fontSize: 13, color: colorScheme.outline),
),
],
),
),
],
),
),
);
}

View File

@@ -0,0 +1,19 @@
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show MainListReply, VoteCard, ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
mixin ReplyVoteMixin on CommonListController<MainListReply, ReplyInfo> {
VoteCard? voteCard;
@override
bool customHandleResponse(bool isRefresh, Success<MainListReply> response) {
if (isRefresh) {
final res = response.response;
if (res.hasVoteCard()) {
voteCard = res.voteCard;
}
}
return super.customHandleResponse(isRefresh, response);
}
}

View File

@@ -14,7 +14,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart'; import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, ReplyControl, Content, Url; show ReplyInfo, ReplyControl, Content, Url, ReplyControl_VoteOption;
import 'package:PiliPlus/grpc/reply.dart'; import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart'; import 'package:PiliPlus/http/reply.dart';
@@ -92,7 +92,7 @@ class ReplyItemGrpc extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final colorScheme = ColorScheme.of(context);
void showMore() => showModalBottomSheet( void showMore() => showModalBottomSheet(
context: context, context: context,
@@ -113,7 +113,7 @@ class ReplyItemGrpc extends StatelessWidget {
Widget child = Padding( Widget child = Padding(
padding: const .fromLTRB(12, 14, 8, 5), padding: const .fromLTRB(12, 14, 8, 5),
child: _buildContent(context, theme), child: _buildContent(context, colorScheme),
); );
if (needDivider) { if (needDivider) {
child = Column( child = Column(
@@ -124,7 +124,7 @@ class ReplyItemGrpc extends StatelessWidget {
indent: 55, indent: 55,
endIndent: 15, endIndent: 15,
height: 0.3, height: 0.3,
color: theme.colorScheme.outline.withValues(alpha: 0.08), color: colorScheme.outline.withValues(alpha: 0.08),
), ),
], ],
); );
@@ -140,7 +140,7 @@ class ReplyItemGrpc extends StatelessWidget {
); );
} }
Widget _buildHeader(BuildContext context, ThemeData theme) { Widget _buildHeader(BuildContext context, ColorScheme colorScheme) {
final member = replyItem.member; final member = replyItem.member;
Widget header = GestureDetector( Widget header = GestureDetector(
onTap: () { onTap: () {
@@ -178,8 +178,8 @@ class ReplyItemGrpc extends StatelessWidget {
overflow: .ellipsis, overflow: .ellipsis,
style: TextStyle( style: TextStyle(
color: (member.vipStatus > 0 && member.vipType == 2) color: (member.vipStatus > 0 && member.vipType == 2)
? theme.colorScheme.vipColor ? colorScheme.vipColor
: theme.colorScheme.outline, : colorScheme.outline,
fontSize: 13, fontSize: 13,
), ),
), ),
@@ -227,16 +227,16 @@ class ReplyItemGrpc extends StatelessWidget {
replyItem.ctime.toInt(), replyItem.ctime.toInt(),
), ),
style: TextStyle( style: TextStyle(
fontSize: theme.textTheme.labelSmall!.fontSize, fontSize: 11,
color: theme.colorScheme.outline, color: colorScheme.outline,
), ),
), ),
if (replyItem.replyControl.hasLocation()) if (replyItem.replyControl.hasLocation())
Text( Text(
'${replyItem.replyControl.location}', '${replyItem.replyControl.location}',
style: TextStyle( style: TextStyle(
fontSize: theme.textTheme.labelSmall!.fontSize, fontSize: 11,
color: theme.colorScheme.outline, color: colorScheme.outline,
), ),
), ),
], ],
@@ -293,23 +293,56 @@ class ReplyItemGrpc extends StatelessWidget {
return header; return header;
} }
Widget _buildContent(BuildContext context, ThemeData theme) { Widget _buildVoteOption(
ColorScheme colorScheme,
ReplyControl_VoteOption voteOption,
) {
return Text.rich(
TextSpan(
children: [
switch (voteOption.labelKind) {
.RED => TextSpan(
text: '红队 ',
style: TextStyle(color: colorScheme.vipColor),
),
.BLUE => TextSpan(
text: '蓝队 ',
style: TextStyle(color: colorScheme.blue),
),
_ => TextSpan(
text: '投票 ',
style: TextStyle(color: colorScheme.outline),
),
},
TextSpan(text: voteOption.desc),
],
),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
);
}
Widget _buildContent(BuildContext context, ColorScheme colorScheme) {
final replyControl = replyItem.replyControl; final replyControl = replyItem.replyControl;
final padding = EdgeInsets.only(left: replyLevel == 0 ? 6 : 45, right: 6); final padding = EdgeInsets.only(left: replyLevel == 0 ? 6 : 45, right: 6);
return Column( return Column(
mainAxisSize: .min, mainAxisSize: .min,
crossAxisAlignment: .start, crossAxisAlignment: .start,
children: [ children: [
_buildHeader(context, theme), _buildHeader(context, colorScheme),
const SizedBox(height: 10), const SizedBox(height: 10),
if (replyControl.hasVoteOption())
Padding(
padding: padding,
child: _buildVoteOption(colorScheme, replyControl.voteOption),
),
Padding( Padding(
padding: padding, padding: padding,
child: custom_text.Text.rich( child: custom_text.Text.rich(
primary: theme.colorScheme.primary, primary: colorScheme.primary,
style: TextStyle( style: const TextStyle(height: 1.75, fontSize: 14),
height: 1.75,
fontSize: theme.textTheme.bodyMedium!.fontSize,
),
maxLines: replyLevel == 1 ? replyLengthLimit : null, maxLines: replyLevel == 1 ? replyLengthLimit : null,
TextSpan( TextSpan(
children: [ children: [
@@ -329,7 +362,7 @@ class ReplyItemGrpc extends StatelessWidget {
], ],
_buildMessage( _buildMessage(
context, context,
theme, colorScheme,
replyControl.showTranslation replyControl.showTranslation
? replyItem.translatedContent ? replyItem.translatedContent
: replyItem.content, : replyItem.content,
@@ -359,12 +392,12 @@ class ReplyItemGrpc extends StatelessWidget {
], ],
if (replyLevel != 0) ...[ if (replyLevel != 0) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
buttonAction(context, theme, replyControl), buttonAction(context, colorScheme, replyControl),
], ],
if (replyLevel == 1 && replyItem.count > Int64.ZERO) ...[ if (replyLevel == 1 && replyItem.count > Int64.ZERO) ...[
Padding( Padding(
padding: const EdgeInsets.only(top: 5, bottom: 12), padding: const EdgeInsets.only(top: 5, bottom: 12),
child: replyItemRow(context, theme, replyItem.replies), child: replyItemRow(context, colorScheme, replyItem.replies),
), ),
], ],
], ],
@@ -373,15 +406,15 @@ class ReplyItemGrpc extends StatelessWidget {
Widget _buildTranslateBtn( Widget _buildTranslateBtn(
BuildContext context, BuildContext context,
ThemeData theme, ColorScheme colorScheme,
ReplyControl replyControl, ReplyControl replyControl,
TextStyle textStyle, TextStyle textStyle,
ButtonStyle buttonStyle, ButtonStyle buttonStyle,
) { ) {
late bool isProcessing = false; late bool isProcessing = false;
final color = replyControl.showTranslation final color = replyControl.showTranslation
? theme.colorScheme.primary ? colorScheme.primary
: theme.colorScheme.outline.withValues(alpha: 0.8); : colorScheme.outline.withValues(alpha: 0.8);
return SizedBox( return SizedBox(
height: 32, height: 32,
child: TextButton( child: TextButton(
@@ -439,14 +472,14 @@ class ReplyItemGrpc extends StatelessWidget {
Widget buttonAction( Widget buttonAction(
BuildContext context, BuildContext context,
ThemeData theme, ColorScheme colorScheme,
ReplyControl replyControl, ReplyControl replyControl,
) { ) {
final textStyle = TextStyle( final textStyle = TextStyle(
height: 1, height: 1,
fontSize: 12,
fontWeight: .normal, fontWeight: .normal,
color: theme.colorScheme.outline, color: colorScheme.outline,
fontSize: theme.textTheme.labelMedium!.fontSize,
); );
const buttonStyle = ButtonStyle( const buttonStyle = ButtonStyle(
visualDensity: .compact, visualDensity: .compact,
@@ -492,7 +525,7 @@ class ReplyItemGrpc extends StatelessWidget {
Icon( Icon(
Icons.reply, Icons.reply,
size: 18, size: 18,
color: theme.colorScheme.outline.withValues(alpha: 0.8), color: colorScheme.outline.withValues(alpha: 0.8),
), ),
Text('回复', style: textStyle), Text('回复', style: textStyle),
], ],
@@ -504,7 +537,7 @@ class ReplyItemGrpc extends StatelessWidget {
.TRANSLATION_SWITCH_SHOW_TRANSLATION) ...[ .TRANSLATION_SWITCH_SHOW_TRANSLATION) ...[
_buildTranslateBtn( _buildTranslateBtn(
context, context,
theme, colorScheme,
replyControl, replyControl,
textStyle, textStyle,
buttonStyle, buttonStyle,
@@ -515,7 +548,7 @@ class ReplyItemGrpc extends StatelessWidget {
dialogBtn != null dialogBtn != null
? replyControl.cardLabels.first.textContent ? replyControl.cardLabels.first.textContent
: replyControl.cardLabels.map((e) => e.textContent).join(' '), : replyControl.cardLabels.map((e) => e.textContent).join(' '),
style: textStyle.copyWith(color: theme.colorScheme.secondary), style: textStyle.copyWith(color: colorScheme.secondary),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
], ],
@@ -529,7 +562,7 @@ class ReplyItemGrpc extends StatelessWidget {
Widget replyItemRow( Widget replyItemRow(
BuildContext context, BuildContext context,
ThemeData theme, ColorScheme colorScheme,
List<ReplyInfo> replies, List<ReplyInfo> replies,
) { ) {
final extraRow = replies.length < replyItem.count.toInt(); final extraRow = replies.length < replyItem.count.toInt();
@@ -537,7 +570,7 @@ class ReplyItemGrpc extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 42, right: 4), padding: const EdgeInsets.only(left: 42, right: 4),
child: Material( child: Material(
color: theme.colorScheme.onInverseSurface, color: colorScheme.onInverseSurface,
borderRadius: const BorderRadius.all(Radius.circular(6)), borderRadius: const BorderRadius.all(Radius.circular(6)),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
animationDuration: Duration.zero, animationDuration: Duration.zero,
@@ -584,11 +617,9 @@ class ReplyItemGrpc extends StatelessWidget {
padding: padding, padding: padding,
child: Text.rich( child: Text.rich(
style: TextStyle( style: TextStyle(
fontSize: theme.textTheme.bodyMedium!.fontSize,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.85,
),
height: 1.6, height: 1.6,
fontSize: 14,
color: colorScheme.onSurface.withValues(alpha: 0.85),
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
@@ -596,9 +627,7 @@ class ReplyItemGrpc extends StatelessWidget {
children: [ children: [
TextSpan( TextSpan(
text: childReply.member.name, text: childReply.member.name,
style: TextStyle( style: TextStyle(color: colorScheme.primary),
color: theme.colorScheme.primary,
),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () { ..onTap = () {
feedBack(); feedBack();
@@ -630,7 +659,7 @@ class ReplyItemGrpc extends StatelessWidget {
), ),
_buildMessage( _buildMessage(
context, context,
theme, colorScheme,
childReply.content, childReply.content,
childReply.replyControl, childReply.replyControl,
), ),
@@ -649,15 +678,13 @@ class ReplyItemGrpc extends StatelessWidget {
: const EdgeInsets.fromLTRB(8, 5, 8, 8), : const EdgeInsets.fromLTRB(8, 5, 8, 8),
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
style: TextStyle( style: const TextStyle(fontSize: 12),
fontSize: theme.textTheme.labelMedium!.fontSize,
),
children: [ children: [
if (replyItem.replyControl.upReply) if (replyItem.replyControl.upReply)
TextSpan( TextSpan(
text: 'UP主等人 ', text: 'UP主等人 ',
style: TextStyle( style: TextStyle(
color: theme.colorScheme.onSurface.withValues( color: colorScheme.onSurface.withValues(
alpha: 0.85, alpha: 0.85,
), ),
), ),
@@ -665,7 +692,7 @@ class ReplyItemGrpc extends StatelessWidget {
TextSpan( TextSpan(
text: '${replyItem.count}条回复', text: '${replyItem.count}条回复',
style: TextStyle( style: TextStyle(
color: theme.colorScheme.primary, color: colorScheme.primary,
), ),
), ),
], ],
@@ -681,7 +708,7 @@ class ReplyItemGrpc extends StatelessWidget {
InlineSpan _buildMessage( InlineSpan _buildMessage(
BuildContext context, BuildContext context,
ThemeData theme, ColorScheme colorScheme,
Content content, Content content,
ReplyControl replyControl, ReplyControl replyControl,
) { ) {
@@ -727,16 +754,14 @@ class ReplyItemGrpc extends StatelessWidget {
child: CachedNetworkImage( child: CachedNetworkImage(
height: 19, height: 19,
memCacheHeight: 19.cacheSize(context), memCacheHeight: 19.cacheSize(context),
color: theme.colorScheme.primary, color: colorScheme.primary,
imageUrl: ImageUtils.thumbnailUrl(url.prefixIcon), imageUrl: ImageUtils.thumbnailUrl(url.prefixIcon),
placeholder: (_, _) => const SizedBox.shrink(), placeholder: (_, _) => const SizedBox.shrink(),
), ),
), ),
TextSpan( TextSpan(
text: isCv ? '[笔记] ' : url.title, text: isCv ? '[笔记] ' : url.title,
style: TextStyle( style: TextStyle(color: colorScheme.primary),
color: theme.colorScheme.primary,
),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () { ..onTap = () {
if (url.appUrlSchema.isEmpty) { if (url.appUrlSchema.isEmpty) {
@@ -815,7 +840,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: matchStr, text: matchStr,
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: colorScheme.primary),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () => ..onTap = () =>
Get.toNamed('/member?mid=${content.atNameToMid[name]}'), Get.toNamed('/member?mid=${content.atNameToMid[name]}'),
@@ -825,7 +850,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: '投票: ${content.vote.title}', text: '投票: ${content.vote.title}',
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: colorScheme.primary),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () => ..onTap = () =>
showVoteDialog(context, content.vote.id.toInt()), showVoteDialog(context, content.vote.id.toInt()),
@@ -847,9 +872,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: isValid ? ' $matchStr ' : matchStr, text: isValid ? ' $matchStr ' : matchStr,
style: isValid style: isValid ? TextStyle(color: colorScheme.primary) : null,
? TextStyle(color: theme.colorScheme.primary)
: null,
recognizer: isValid recognizer: isValid
? (NoDeadlineTapGestureRecognizer() ? (NoDeadlineTapGestureRecognizer()
..onTap = () { ..onTap = () {
@@ -881,7 +904,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: matchStr, text: matchStr,
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: colorScheme.primary),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () { ..onTap = () {
Get.toNamed( Get.toNamed(
@@ -895,7 +918,7 @@ class ReplyItemGrpc extends StatelessWidget {
spanChildren.add( spanChildren.add(
TextSpan( TextSpan(
text: matchStr, text: matchStr,
style: TextStyle(color: theme.colorScheme.primary), style: TextStyle(color: colorScheme.primary),
recognizer: NoDeadlineTapGestureRecognizer() recognizer: NoDeadlineTapGestureRecognizer()
..onTap = () => PageUtils.handleWebview(matchStr), ..onTap = () => PageUtils.handleWebview(matchStr),
), ),
@@ -929,7 +952,7 @@ class ReplyItemGrpc extends StatelessWidget {
final hasClickUrl = content.richText.note.hasClickUrl(); final hasClickUrl = content.richText.note.hasClickUrl();
if (hasClickUrl || content.richText.opus.hasOpusId()) { if (hasClickUrl || content.richText.opus.hasOpusId()) {
color = theme.colorScheme.primary; color = colorScheme.primary;
recognizer = NoDeadlineTapGestureRecognizer() recognizer = NoDeadlineTapGestureRecognizer()
..onTap = () => hasClickUrl ..onTap = () => hasClickUrl
? PiliScheme.routePushFromUrl(content.richText.note.clickUrl) ? PiliScheme.routePushFromUrl(content.richText.note.clickUrl)
@@ -941,7 +964,7 @@ class ReplyItemGrpc extends StatelessWidget {
}, },
); );
} else { } else {
color = theme.colorScheme.secondary; color = colorScheme.secondary;
} }
spanChildren.insert( spanChildren.insert(
0, 0,
@@ -965,11 +988,12 @@ class ReplyItemGrpc extends StatelessWidget {
late String message = item.content.message; late String message = item.content.message;
final ownerMid = Int64(Accounts.main.mid); final ownerMid = Int64(Accounts.main.mid);
final theme = Theme.of(context); final theme = Theme.of(context);
final errorColor = theme.colorScheme.error; final colorScheme = theme.colorScheme;
final errorColor = colorScheme.error;
final style = theme.textTheme.titleSmall!; final style = theme.textTheme.titleSmall!;
return Padding( return Padding(
padding: EdgeInsets.only( padding: .only(
bottom: MediaQuery.viewPaddingOf(context).bottom + 20, bottom: MediaQuery.viewPaddingOf(context).bottom + 20,
), ),
child: Column( child: Column(
@@ -985,7 +1009,7 @@ class ReplyItemGrpc extends StatelessWidget {
width: 32, width: 32,
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.outline, color: colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3)), borderRadius: const BorderRadius.all(Radius.circular(3)),
), ),
), ),
@@ -1007,7 +1031,7 @@ class ReplyItemGrpc extends StatelessWidget {
}, },
title: Text( title: Text(
'save to local', 'save to local',
style: style.copyWith(color: theme.colorScheme.primary), style: style.copyWith(color: colorScheme.primary),
), ),
), ),
ListTile( ListTile(
@@ -1018,7 +1042,7 @@ class ReplyItemGrpc extends StatelessWidget {
}, },
title: Text( title: Text(
'remove from local', 'remove from local',
style: style.copyWith(color: theme.colorScheme.primary), style: style.copyWith(color: colorScheme.primary),
), ),
), ),
ListTile( ListTile(
@@ -1037,7 +1061,7 @@ class ReplyItemGrpc extends StatelessWidget {
}, },
title: Text( title: Text(
'save to local (x1000)', 'save to local (x1000)',
style: style.copyWith(color: theme.colorScheme.primary), style: style.copyWith(color: colorScheme.primary),
), ),
), ),
], ],
@@ -1048,7 +1072,7 @@ class ReplyItemGrpc extends StatelessWidget {
bool? isDelete = await showDialog<bool>( bool? isDelete = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
final theme = Theme.of(context); final colorScheme = ColorScheme.of(context);
return AlertDialog( return AlertDialog(
title: const Text('删除评论'), title: const Text('删除评论'),
content: Text.rich( content: Text.rich(
@@ -1059,7 +1083,7 @@ class ReplyItemGrpc extends StatelessWidget {
TextSpan( TextSpan(
text: '@${item.member.name}', text: '@${item.member.name}',
style: TextStyle( style: TextStyle(
color: theme.colorScheme.primary, color: colorScheme.primary,
), ),
), ),
const TextSpan(text: ':\n'), const TextSpan(text: ':\n'),
@@ -1074,7 +1098,7 @@ class ReplyItemGrpc extends StatelessWidget {
child: Text( child: Text(
'取消', '取消',
style: TextStyle( style: TextStyle(
color: theme.colorScheme.outline, color: colorScheme.outline,
), ),
), ),
), ),

View File

@@ -5,6 +5,9 @@ import 'package:flutter/material.dart'
const _pinkLight = Color(0xFFFF6699); const _pinkLight = Color(0xFFFF6699);
const _pinkDark = Color(0xFFD44E7D); const _pinkDark = Color(0xFFD44E7D);
const _blueLight = Color(0xFF008AC5);
const _blueDark = Color(0xFF2C9CC8);
extension ThemeDataExt on ThemeData { extension ThemeDataExt on ThemeData {
bool get isLight => brightness.isLight; bool get isLight => brightness.isLight;
@@ -14,6 +17,8 @@ extension ThemeDataExt on ThemeData {
extension ColorSchemeExt on ColorScheme { extension ColorSchemeExt on ColorScheme {
Color get vipColor => brightness.isLight ? _pinkLight : _pinkDark; Color get vipColor => brightness.isLight ? _pinkLight : _pinkDark;
Color get blue => brightness.isLight ? _blueLight : _blueDark;
Color get btnColor => Color get btnColor =>
brightness.isLight ? _pinkLight : const Color(0xFF8F0030); brightness.isLight ? _pinkLight : const Color(0xFF8F0030);