feat: pgc review

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-24 18:17:57 +08:00
parent 8e1b2be073
commit 70164fa3f7
18 changed files with 1140 additions and 108 deletions

View File

@@ -0,0 +1,89 @@
import 'package:PiliPlus/http/bangumi.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/pgc_review/data.dart';
import 'package:PiliPlus/models/bangumi/pgc_review/list.dart';
import 'package:PiliPlus/models/common/pgc_review_type.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class PgcReviewController
extends CommonListController<PgcReviewData, PgcReviewItemModel> {
PgcReviewController({required this.type, required this.mediaId});
final PgcReviewType type;
final dynamic mediaId;
int? count;
String? next;
@override
void onInit() {
super.onInit();
queryData();
}
@override
Future<void> onRefresh() {
count = null;
next = null;
return super.onRefresh();
}
@override
void checkIsEnd(int length) {
if (count != null && length >= count!) {
isEnd = true;
}
}
@override
List<PgcReviewItemModel>? getDataList(PgcReviewData response) {
count = response.count;
next = response.next;
return response.list;
}
@override
Future<LoadingState<PgcReviewData>> customGetData() => BangumiHttp.pgcReview(
type: type,
mediaId: mediaId,
next: next,
);
Future<void> onLike(int index, bool isLike, reviewId) async {
var res = await BangumiHttp.pgcReviewLike(
mediaId: mediaId,
reviewId: reviewId,
);
if (res['status']) {
final item = loadingState.value.data![index];
int likes = item.stat?.likes ?? 0;
item.stat
?..liked = isLike ? 0 : 1
..likes = isLike ? likes - 1 : likes + 1;
if (!isLike) {
item.stat?.disliked = 0;
}
loadingState.refresh();
} else {
SmartDialog.showToast(res['msg']);
}
}
Future<void> onDislike(int index, bool isDislike, reviewId) async {
var res = await BangumiHttp.pgcReviewDislike(
mediaId: mediaId,
reviewId: reviewId,
);
if (res['status']) {
final item = loadingState.value.data![index];
item.stat?.disliked = isDislike ? 0 : 1;
if (!isDislike) {
item.stat?.liked = 0;
}
loadingState.refresh();
} else {
SmartDialog.showToast(res['msg']);
}
}
}

View File

@@ -0,0 +1,298 @@
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/pgc_review/list.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/common/pgc_review_type.dart';
import 'package:PiliPlus/pages/pgc_review/child/controller.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
class PgcReviewChildPage extends StatefulWidget {
const PgcReviewChildPage({
super.key,
required this.type,
required this.mediaId,
});
final PgcReviewType type;
final dynamic mediaId;
@override
State<PgcReviewChildPage> createState() => _PgcReviewChildPageState();
}
class _PgcReviewChildPageState extends State<PgcReviewChildPage>
with AutomaticKeepAliveClientMixin {
late final _controller = Get.put(
PgcReviewController(type: widget.type, mediaId: widget.mediaId),
tag: '${widget.mediaId}${widget.type.name}',
);
late final isLongReview = widget.type == PgcReviewType.long;
@override
void dispose() {
Get.delete<PgcReviewController>(
tag: '${widget.mediaId}${widget.type.name}');
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
final theme = Theme.of(context);
return refreshIndicator(
onRefresh: _controller.onRefresh,
child: CustomScrollView(
controller: _controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80),
sliver:
Obx(() => _buildBody(theme, _controller.loadingState.value)),
),
],
),
);
}
Widget _buildBody(
ThemeData theme, LoadingState<List<PgcReviewItemModel>?> loadingState) {
return switch (loadingState) {
Loading() => SliverToBoxAdapter(
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return const VideoReplySkeleton();
},
itemCount: 8,
),
),
Success(:var response) => response?.isNotEmpty == true
? SliverList.separated(
itemBuilder: (context, index) {
if (index == response.length - 1) {
_controller.onLoadMore();
}
return _itemWidget(theme, index, response[index]);
},
itemCount: response!.length,
separatorBuilder: (context, index) => Divider(
height: 1,
color: theme.colorScheme.outline.withValues(alpha: 0.1),
),
)
: HttpError(onReload: _controller.onReload),
Error(:var errMsg) => HttpError(
errMsg: errMsg,
onReload: _controller.onReload,
),
};
}
Widget _itemWidget(ThemeData theme, int index, PgcReviewItemModel item) {
return InkWell(
onTap: isLongReview
? () => Get.toNamed(
'/articlePage',
parameters: {
'id': item.articleId!.toString(),
'type': 'read',
},
)
: null,
onLongPress: isLongReview
? null
: () => showConfirmDialog(
context: context,
title: '确定举报该点评?',
onConfirm: () => Get.toNamed(
'/webview',
parameters: {
'url':
'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}'
},
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Get.toNamed('/member?mid=${item.author!.mid}'),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
NetworkImgLayer(
height: 34,
width: 34,
src: item.author!.avatar,
type: ImageType.avatar,
),
const SizedBox(width: 10),
Column(
spacing: 2,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item.author!.uname!,
style: TextStyle(
color: item.author?.vip?.status != null &&
item.author!.vip!.status > 0 &&
item.author!.vip!.type == 2
? context.vipColor
: theme.colorScheme.outline,
fontSize: 13,
),
),
Image.asset(
'assets/images/lv/lv${item.author!.level}.png',
height: 11,
),
],
),
Row(
children: [
if (item.pushTimeStr != null) ...[
Text(
item.pushTimeStr!,
style: TextStyle(
color: theme.colorScheme.outline,
fontSize: 12,
),
),
const SizedBox(width: 10),
],
...List.generate(
5,
(index) {
if (index <= item.score - 1) {
return const Icon(
CustomIcon.star_favorite_solid,
size: 13,
color: Color(0xFFFFAD35),
);
}
return const Icon(
CustomIcon.star_favorite_line,
size: 14,
color: Colors.grey,
);
},
),
],
)
],
)
],
),
),
const SizedBox(height: 5),
if (item.title != null)
Text(
item.title!,
style: const TextStyle(
height: 1.75,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
if (isLongReview)
Text(
item.content!,
style: const TextStyle(height: 1.75),
)
else
SelectableText(
item.content!,
style: const TextStyle(height: 1.75),
),
Builder(
builder: (context) {
final Color color = theme.colorScheme.outline;
final Color primary = theme.colorScheme.primary;
final ButtonStyle style = TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
);
final isLike = item.stat?.liked == 1;
late final isDislike = item.stat?.disliked == 1;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!isLongReview)
SizedBox(
height: 32,
child: TextButton(
style: style,
onPressed: () => _controller.onDislike(
index, isDislike, item.reviewId),
child: Icon(
isDislike
? FontAwesomeIcons.solidThumbsDown
: FontAwesomeIcons.thumbsDown,
size: 16,
color: isDislike ? primary : color,
),
),
),
SizedBox(
height: 32,
child: TextButton(
style: style,
onPressed: isLongReview
? null
: () => _controller.onLike(
index, isLike, item.reviewId),
child: Row(
spacing: 4,
children: [
Icon(
isLike
? FontAwesomeIcons.solidThumbsUp
: FontAwesomeIcons.thumbsUp,
size: 16,
color: isLike ? primary : color,
),
Text(
Utils.numFormat(item.stat?.likes ?? 0),
style: TextStyle(
color: isLike ? primary : color,
fontSize: 12,
),
),
],
),
),
),
],
);
},
),
],
),
),
);
}
@override
bool get wantKeepAlive => true;
}