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/http/user.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/model_video.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),
() => Utils.copyText(videoItem.bvid!),
),
_VideoCustomAction(
'稍后再看',
const Icon(MdiIcons.clockTimeEightOutline, size: 16),
() => UserHttp.toViewLater(bvid: videoItem.bvid),
),
if (Accounts.main.isLogin)
_VideoCustomAction(
'稍后再看',
const Icon(MdiIcons.clockTimeEightOutline, size: 16),
() => UserHttp.toViewLater(bvid: videoItem.bvid),
),
if (videoItem.cid != null && Pref.enableAi)
_VideoCustomAction(
'AI总结',
@@ -100,11 +100,11 @@ class VideoPopupMenu extends StatelessWidget {
'不感兴趣',
const Icon(MdiIcons.thumbDownOutline, size: 16),
() {
String? accessKey = Accounts.get(
AccountType.recommend,
).accessKey;
if (accessKey == null || accessKey == "") {
SmartDialog.showToast("请退出账号后重新登录");
final rcmd = Accounts.get(.recommend);
if (rcmd.accessKey == null || rcmd.accessKey == "") {
SmartDialog.showToast(
rcmd.isLogin ? '请退出账号后重新登录' : '账号未登录',
);
return;
}
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 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']);
}
}
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/utils/parse_string.dart';
@@ -14,6 +15,7 @@ class CardLiveItem {
int? areaV2Id;
int? areaV2ParentId;
WatchedShow? watchedShow;
List<Feedback>? feedback;
CardLiveItem({
this.roomid,
@@ -27,6 +29,7 @@ class CardLiveItem {
this.areaV2Id,
this.areaV2ParentId,
this.watchedShow,
this.feedback,
}) : _systemCover = nonNullOrEmptyString(systemCover);
factory CardLiveItem.fromJson(Map<String, dynamic> json) => CardLiveItem(
@@ -43,5 +46,8 @@ class CardLiveItem {
watchedShow: json['watched_show'] == null
? null
: 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/widgets/image/image_save.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/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/platform_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
// 视频卡片 - 垂直布局
class LiveCardVApp extends StatelessWidget {
@@ -19,57 +25,139 @@ class LiveCardVApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
void onLongPress() => imageSaveDialog(
title: item.title,
cover: showFirstFrame ? item.systemCover : item.cover,
);
return Card(
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => PageUtils.toLiveRoom(item.roomid),
onLongPress: onLongPress,
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
child: Column(
children: [
AspectRatio(
aspectRatio: Style.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: showFirstFrame ? item.systemCover : item.cover,
width: maxWidth,
height: maxHeight,
type: .emote,
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedOpacity(
opacity: 1,
duration: const Duration(milliseconds: 200),
child: videoStat(context),
return Stack(
children: [
Card(
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => PageUtils.toLiveRoom(item.roomid),
onLongPress: onLongPress,
onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress,
child: Column(
children: [
AspectRatio(
aspectRatio: Style.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) => Stack(
clipBehavior: Clip.none,
children: [
NetworkImgLayer(
src: showFirstFrame ? item.systemCover : item.cover,
width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight,
type: .emote,
),
),
],
);
},
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) {
final theme = Theme.of(context);
Widget liveContent(ThemeData theme) {
return Expanded(
flex: 1,
child: Padding(
@@ -79,29 +167,24 @@ class LiveCardVApp extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${item.title}',
item.title.toString(),
textAlign: TextAlign.start,
style: const TextStyle(
letterSpacing: 0.3,
),
style: const TextStyle(letterSpacing: 0.3),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Expanded(
child: Text(
'${item.uname}',
textAlign: TextAlign.start,
style: TextStyle(
fontSize: theme.textTheme.labelMedium!.fontSize,
color: theme.colorScheme.outline,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Align(
alignment: .topLeft,
child: Text(
item.uname.toString(),
textAlign: TextAlign.start,
style: TextStyle(
fontSize: theme.textTheme.labelMedium!.fontSize,
color: theme.colorScheme.outline,
),
],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
@@ -109,7 +192,7 @@ class LiveCardVApp extends StatelessWidget {
);
}
Widget videoStat(BuildContext context) {
Widget videoStat() {
return Container(
height: 50,
padding: const EdgeInsets.only(top: 26, left: 10, right: 10),
@@ -117,10 +200,7 @@ class LiveCardVApp extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
colors: <Color>[Colors.transparent, Colors.black54],
tileMode: TileMode.mirror,
),
),
@@ -128,7 +208,7 @@ class LiveCardVApp extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${item.areaName}',
item.areaName.toString(),
style: const TextStyle(fontSize: 11, color: Colors.white),
),
if (item.watchedShow?.textLarge case final textLarge?)

View File

@@ -177,7 +177,7 @@ class AccountManager extends Interceptor {
'site/getCoin',
];
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)) ||
(url.contains('skipSegments') && err.requestOptions.method == 'GET')) {
// skip

View File

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