feat: live feedback (#2456)

This commit is contained in:
My-Responsitories
2026-07-21 08:49:37 +00:00
committed by GitHub
parent 2d389424af
commit 3a5636ecee
8 changed files with 251 additions and 77 deletions

View File

@@ -1,7 +1,6 @@
import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/common/account_type.dart';
import 'package:PiliPlus/models/home/rcmd/result.dart'; import 'package:PiliPlus/models/home/rcmd/result.dart';
import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models/model_video.dart';
import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart';
@@ -56,11 +55,12 @@ class VideoPopupMenu extends StatelessWidget {
const Icon(CustomIcons.identifier_circle, size: 16), const Icon(CustomIcons.identifier_circle, size: 16),
() => Utils.copyText(videoItem.bvid!), () => Utils.copyText(videoItem.bvid!),
), ),
_VideoCustomAction( if (Accounts.main.isLogin)
'稍后再看', _VideoCustomAction(
const Icon(MdiIcons.clockTimeEightOutline, size: 16), '稍后再看',
() => UserHttp.toViewLater(bvid: videoItem.bvid), const Icon(MdiIcons.clockTimeEightOutline, size: 16),
), () => UserHttp.toViewLater(bvid: videoItem.bvid),
),
if (videoItem.cid != null && Pref.enableAi) if (videoItem.cid != null && Pref.enableAi)
_VideoCustomAction( _VideoCustomAction(
'AI总结', 'AI总结',
@@ -100,11 +100,11 @@ class VideoPopupMenu extends StatelessWidget {
'不感兴趣', '不感兴趣',
const Icon(MdiIcons.thumbDownOutline, size: 16), const Icon(MdiIcons.thumbDownOutline, size: 16),
() { () {
String? accessKey = Accounts.get( final rcmd = Accounts.get(.recommend);
AccountType.recommend, if (rcmd.accessKey == null || rcmd.accessKey == "") {
).accessKey; SmartDialog.showToast(
if (accessKey == null || accessKey == "") { rcmd.isLogin ? '请退出账号后重新登录' : '账号未登录',
SmartDialog.showToast("请退出账号后重新登录"); );
return; return;
} }
if (videoItem case final RcmdVideoItemAppModel item) { if (videoItem case final RcmdVideoItemAppModel item) {

View File

@@ -1014,4 +1014,7 @@ abstract final class Api {
static const String replyReport = '/x/v2/reply/report'; static const String replyReport = '/x/v2/reply/report';
static const String dynReaction = '/x/polymer/web-dynamic/v1/detail/reaction'; static const String dynReaction = '/x/polymer/web-dynamic/v1/detail/reaction';
static const String liveFeedback =
'${HttpString.liveBaseUrl}/xlive/app-interface/v2/index/feedback';
} }

View File

@@ -765,4 +765,41 @@ abstract final class LiveHttp {
return Error(res.data['message']); return Error(res.data['message']);
} }
} }
static Future<LoadingState<void>> liveFeedback(
Object roomId,
Object id,
String type, {
int page = 1,
}) async {
final params = {
'access_key': ?recommend.accessKey,
'actionKey': 'appkey',
'build': 8430300,
'channel': 'master',
'c_locale': 'zh_CN',
'device': 'android',
'disable_rcmd': 0,
'mobi_app': 'android',
'platform': 'android',
's_locale': 'zh_CN',
'statistics': Constants.statisticsApp,
'version': '8.43.0',
'id': id,
'id_type': type,
'room_id': roomId,
'type': 'dislike',
'page': page,
};
AppSign.appSign(params);
final res = await Request().get(
Api.liveFeedback,
queryParameters: params,
);
if (res.data['code'] == 0) {
return const Success(null);
} else {
return Error(res.data['message']);
}
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models_new/live/live_feed_index/feedback.dart';
import 'package:PiliPlus/models_new/live/live_feed_index/watched_show.dart'; import 'package:PiliPlus/models_new/live/live_feed_index/watched_show.dart';
import 'package:PiliPlus/utils/parse_string.dart'; import 'package:PiliPlus/utils/parse_string.dart';
@@ -14,6 +15,7 @@ class CardLiveItem {
int? areaV2Id; int? areaV2Id;
int? areaV2ParentId; int? areaV2ParentId;
WatchedShow? watchedShow; WatchedShow? watchedShow;
List<Feedback>? feedback;
CardLiveItem({ CardLiveItem({
this.roomid, this.roomid,
@@ -27,6 +29,7 @@ class CardLiveItem {
this.areaV2Id, this.areaV2Id,
this.areaV2ParentId, this.areaV2ParentId,
this.watchedShow, this.watchedShow,
this.feedback,
}) : _systemCover = nonNullOrEmptyString(systemCover); }) : _systemCover = nonNullOrEmptyString(systemCover);
factory CardLiveItem.fromJson(Map<String, dynamic> json) => CardLiveItem( factory CardLiveItem.fromJson(Map<String, dynamic> json) => CardLiveItem(
@@ -43,5 +46,8 @@ class CardLiveItem {
watchedShow: json['watched_show'] == null watchedShow: json['watched_show'] == null
? null ? null
: WatchedShow.fromJson(json['watched_show'] as Map<String, dynamic>), : WatchedShow.fromJson(json['watched_show'] as Map<String, dynamic>),
feedback: (json['feedback'] as List?)
?.map((x) => Feedback.fromJson(x))
.toList(),
); );
} }

View File

@@ -0,0 +1,47 @@
class Feedback {
Feedback({
this.title,
this.subtitle,
this.type,
this.reasons,
});
final String? title;
final String? subtitle;
final String? type;
final List<Reason>? reasons;
factory Feedback.fromJson(Map<String, dynamic> json) {
return Feedback(
title: json['title'],
subtitle: json['subtitle'],
type: json['type'],
reasons: (json['reasons'] as List?)
?.map((x) => Reason.fromJson(x))
.toList(),
);
}
}
class Reason {
Reason({
this.id,
this.name,
this.idType,
this.reasonId,
});
final int? id;
final String? name;
final String? idType;
final int? reasonId;
factory Reason.fromJson(Map<String, dynamic> json) {
return Reason(
id: json['id'],
name: json['name'],
idType: json['id_type'],
reasonId: json['reason_id'],
);
}
}

View File

@@ -1,10 +1,16 @@
import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/live.dart';
import 'package:PiliPlus/models_new/live/live_feed_index/card_data_list_item.dart'; import 'package:PiliPlus/models_new/live/live_feed_index/card_data_list_item.dart';
import 'package:PiliPlus/models_new/live/live_feed_index/feedback.dart';
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
// 视频卡片 - 垂直布局 // 视频卡片 - 垂直布局
class LiveCardVApp extends StatelessWidget { class LiveCardVApp extends StatelessWidget {
@@ -19,57 +25,139 @@ class LiveCardVApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
void onLongPress() => imageSaveDialog( void onLongPress() => imageSaveDialog(
title: item.title, title: item.title,
cover: showFirstFrame ? item.systemCover : item.cover, cover: showFirstFrame ? item.systemCover : item.cover,
); );
return Card( return Stack(
clipBehavior: Clip.hardEdge, children: [
child: InkWell( Card(
onTap: () => PageUtils.toLiveRoom(item.roomid), clipBehavior: Clip.hardEdge,
onLongPress: onLongPress, child: InkWell(
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, onTap: () => PageUtils.toLiveRoom(item.roomid),
child: Column( onLongPress: onLongPress,
children: [ onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
AspectRatio( child: Column(
aspectRatio: Style.aspectRatio, children: [
child: LayoutBuilder( AspectRatio(
builder: (context, boxConstraints) { aspectRatio: Style.aspectRatio,
double maxWidth = boxConstraints.maxWidth; child: LayoutBuilder(
double maxHeight = boxConstraints.maxHeight; builder: (context, boxConstraints) => Stack(
return Stack( clipBehavior: Clip.none,
clipBehavior: Clip.none, children: [
children: [ NetworkImgLayer(
NetworkImgLayer( src: showFirstFrame ? item.systemCover : item.cover,
src: showFirstFrame ? item.systemCover : item.cover, width: boxConstraints.maxWidth,
width: maxWidth, height: boxConstraints.maxHeight,
height: maxHeight, type: .emote,
type: .emote,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: videoStat(context),
), ),
), Positioned(
], left: 0,
); right: 0,
}, bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: videoStat(),
),
),
],
),
),
),
liveContent(theme),
],
),
),
),
if (!item.feedback.isNullOrEmpty)
Positioned(
right: -5,
bottom: -2,
width: 29,
height: 29,
child: IconButton(
padding: .zero,
onPressed: () {
Widget actionButton(Reason r) => SearchText(
text: r.name!,
onTap: (_) async {
Get.back();
SmartDialog.showLoading(msg: '正在提交');
final res = await LiveHttp.liveFeedback(
item.roomid!,
r.id!,
r.idType!,
);
SmartDialog.dismiss();
if (res.isSuccess) {
SmartDialog.showToast('提交成功');
} else {
res.toast();
}
},
);
final feedback = item.feedback!;
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
contentPadding: const .fromLTRB(24, 16, 24, 19),
children: [
for (var i in feedback) ...[
const SizedBox(height: 5),
Text.rich(
TextSpan(
children: [
TextSpan(
text: i.title,
style: theme.textTheme.titleMedium,
),
TextSpan(
text: '\n${i.subtitle}',
style: TextStyle(
color: theme.colorScheme.outline,
),
),
],
),
),
const SizedBox(height: 5),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: i.reasons!.map(actionButton).toList(),
),
],
const Divider(),
Center(
child: FilledButton.tonal(
onPressed: Get.back,
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
),
child: const Text('取消'),
),
),
],
);
},
);
},
icon: Icon(
Icons.more_vert_outlined,
size: 17,
color: theme.colorScheme.outline,
), ),
), ),
liveContent(context), ),
], ],
),
),
); );
} }
Widget liveContent(BuildContext context) { Widget liveContent(ThemeData theme) {
final theme = Theme.of(context);
return Expanded( return Expanded(
flex: 1, flex: 1,
child: Padding( child: Padding(
@@ -79,29 +167,24 @@ class LiveCardVApp extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'${item.title}', item.title.toString(),
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(letterSpacing: 0.3),
letterSpacing: 0.3,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Row( Align(
children: [ alignment: .topLeft,
Expanded( child: Text(
child: Text( item.uname.toString(),
'${item.uname}', textAlign: TextAlign.start,
textAlign: TextAlign.start, style: TextStyle(
style: TextStyle( fontSize: theme.textTheme.labelMedium!.fontSize,
fontSize: theme.textTheme.labelMedium!.fontSize, color: theme.colorScheme.outline,
color: theme.colorScheme.outline,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
], maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),
@@ -109,7 +192,7 @@ class LiveCardVApp extends StatelessWidget {
); );
} }
Widget videoStat(BuildContext context) { Widget videoStat() {
return Container( return Container(
height: 50, height: 50,
padding: const EdgeInsets.only(top: 26, left: 10, right: 10), padding: const EdgeInsets.only(top: 26, left: 10, right: 10),
@@ -117,10 +200,7 @@ class LiveCardVApp extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: <Color>[ colors: <Color>[Colors.transparent, Colors.black54],
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
), ),
), ),
@@ -128,7 +208,7 @@ class LiveCardVApp extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'${item.areaName}', item.areaName.toString(),
style: const TextStyle(fontSize: 11, color: Colors.white), style: const TextStyle(fontSize: 11, color: Colors.white),
), ),
if (item.watchedShow?.textLarge case final textLarge?) if (item.watchedShow?.textLarge case final textLarge?)

View File

@@ -177,7 +177,7 @@ class AccountManager extends Interceptor {
'site/getCoin', 'site/getCoin',
]; ];
String url = err.requestOptions.uri.toString(); String url = err.requestOptions.uri.toString();
if (kDebugMode) debugPrint('🌹🌹ApiInterceptor: $url'); if (kDebugMode) debugPrint('🌹🌹ApiInterceptor: $url\n$err');
if (skipShow.any((i) => url.contains(i)) || if (skipShow.any((i) => url.contains(i)) ||
(url.contains('skipSegments') && err.requestOptions.method == 'GET')) { (url.contains('skipSegments') && err.requestOptions.method == 'GET')) {
// skip // skip

View File

@@ -88,6 +88,7 @@ abstract final class ApiType {
Api.dynTopicRcmd, Api.dynTopicRcmd,
Api.topicFeed, Api.topicFeed,
Api.topicTop, Api.topicTop,
Api.liveFeedback,
}, },
// progress // progress
AccountType.video: { AccountType.video: {