mod: 评论刷新、翻页

This commit is contained in:
guozhigq
2023-04-28 12:48:45 +08:00
parent c6c4138640
commit 6fbfd2db9e
7 changed files with 324 additions and 238 deletions

View File

@ -22,10 +22,12 @@ class ReplyData {
ReplyData.fromJson(Map<String, dynamic> json) { ReplyData.fromJson(Map<String, dynamic> json) {
page = ReplyPage.fromJson(json['page']); page = ReplyPage.fromJson(json['page']);
config = ReplyConfig.fromJson(json['config']); config = ReplyConfig.fromJson(json['config']);
replies = json['replies'] replies = json['replies'] != null
? json['replies']
.map<ReplyItemModel>( .map<ReplyItemModel>(
(item) => ReplyItemModel.fromJson(item, json['upper']['mid'])) (item) => ReplyItemModel.fromJson(item, json['upper']['mid']))
.toList(); .toList()
: [];
topReplies = json['top_replies'] != null topReplies = json['top_replies'] != null
? json['top_replies'] ? json['top_replies']
.map<ReplyItemModel>((item) => ReplyItemModel.fromJson( .map<ReplyItemModel>((item) => ReplyItemModel.fromJson(

View File

@ -1,22 +1,62 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/reply.dart'; import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/video/reply/data.dart'; import 'package:pilipala/models/video/reply/data.dart';
import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyController extends GetxController { class VideoReplyController extends GetxController {
final ScrollController scrollController = ScrollController();
// 视频aid // 视频aid
String aid = Get.parameters['aid']!; String aid = Get.parameters['aid']!;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
// 当前页
int currentPage = 0;
bool isLoadingMore = false;
bool noMore = false;
@override Future queryReplyList({type = 'init'}) async {
void onInit() { isLoadingMore = true;
super.onInit(); var res =
queryReplyList(); await ReplyHttp.replyList(oid: aid, pageNum: currentPage + 1, type: 1);
}
Future queryReplyList() async {
var res = await ReplyHttp.replyList(oid: aid, pageNum: 1, type: 1);
if (res['status']) { if (res['status']) {
res['data'] = ReplyData.fromJson(res['data']); res['data'] = ReplyData.fromJson(res['data']);
if (res['data'].replies.isNotEmpty) {
currentPage = currentPage + 1;
noMore = false;
} else {
if (currentPage == 0) {
} else {
noMore = true;
} }
}
if (type == 'init') {
List<ReplyItemModel> replies = res['data'].replies;
// 添加置顶回复
if (res['data'].upper.top != null) {
bool flag = false;
for (var i = 0; i < res['data'].topReplies.length; i++) {
if (res['data'].topReplies[i].rpid == res['data'].upper.top.rpid) {
flag = true;
}
}
if (!flag) {
replies.insert(0, res['data'].upper.top);
}
}
replies.insertAll(0, res['data'].topReplies);
res['data'].replies = replies;
replyList.value = res['data'].replies!;
} else {
replyList.addAll(res['data'].replies!);
res['data'].replies.addAll(replyList);
}
}
isLoadingMore = false;
return res; return res;
} }
// 上拉加载
Future onLoad() async {
queryReplyList(type: 'onLoad');
}
} }

View File

@ -17,52 +17,80 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final VideoReplyController _videoReplyController = final VideoReplyController _videoReplyController =
Get.put(VideoReplyController(), tag: Get.arguments['heroTag']); Get.put(VideoReplyController(), tag: Get.arguments['heroTag']);
// List<ReplyItemModel>? replyList;
Future? _futureBuilderFuture;
// 添加页面缓存 // 添加页面缓存
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { void initState() {
return FutureBuilder( super.initState();
future: _videoReplyController.queryReplyList(),
builder: (context, snapshot) { _futureBuilderFuture = _videoReplyController.queryReplyList();
if (snapshot.connectionState == ConnectionState.done) { _videoReplyController.scrollController.addListener(
if (snapshot.data['status']) { () {
List<ReplyItemModel> replies = snapshot.data['data'].replies; if (_videoReplyController.scrollController.position.pixels >=
// 添加置顶回复 _videoReplyController.scrollController.position.maxScrollExtent -
if (snapshot.data['data'].upper.top != null) { 300) {
bool flag = false; if (!_videoReplyController.isLoadingMore) {
for (var i = 0; _videoReplyController.onLoad();
i < snapshot.data['data'].topReplies.length;
i++) {
if (snapshot.data['data'].topReplies[i].rpid ==
snapshot.data['data'].upper.top.rpid) {
flag = true;
} }
} }
if (!flag) { },
replies.insert(0, snapshot.data['data'].upper.top); );
}
} }
replies.insertAll(0, snapshot.data['data'].topReplies); @override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
setState(() {});
_videoReplyController.currentPage = 0;
return await _videoReplyController.queryReplyList();
},
child: CustomScrollView(
controller: _videoReplyController.scrollController,
key: const PageStorageKey<String>('评论'),
slivers: <Widget>[
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
// 请求成功 // 请求成功
return SliverList( return Obx(
delegate: SliverChildBuilderDelegate((context, index) { () => SliverList(
if (index == replies.length) { delegate: SliverChildBuilderDelegate(
return SizedBox(height: MediaQuery.of(context).padding.bottom); (context, index) {
if (index == _videoReplyController.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom),
height:
MediaQuery.of(context).padding.bottom + 60,
child: Center(
child: Text(_videoReplyController.noMore
? '没有更多了'
: '加载中'),
),
);
} else { } else {
return ReplyItem( return ReplyItem(
replyItem: replies[index], replyItem: _videoReplyController.replyList[index],
isUp: );
replies[index].mid == snapshot.data['data'].upper.mid);
} }
}, childCount: replies.length + 1)); },
childCount: _videoReplyController.replyList.length + 1,
),
),
);
} else { } else {
// 请求错误 // 请求错误
return HttpError( return HttpError(
errMsg: snapshot.data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () => setState(() {}),
); );
} }
@ -75,6 +103,9 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
); );
} }
}, },
)
],
),
); );
} }
} }

View File

@ -5,9 +5,8 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
class ReplyItem extends StatelessWidget { class ReplyItem extends StatelessWidget {
ReplyItem({super.key, this.replyItem, required this.isUp}); ReplyItem({super.key, this.replyItem});
ReplyItemModel? replyItem; ReplyItemModel? replyItem;
bool isUp = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -45,10 +44,6 @@ class ReplyItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// 头像、昵称 // 头像、昵称
Row(
// 两端对齐
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
GestureDetector( GestureDetector(
// onTap: () => // onTap: () =>
// Get.toNamed('/member/${reply.userName}', parameters: { // Get.toNamed('/member/${reply.userName}', parameters: {
@ -68,20 +63,21 @@ class ReplyItem extends StatelessWidget {
children: [ children: [
Text( Text(
replyItem!.member!.uname!, replyItem!.member!.uname!,
style: Theme.of(context) style: TextStyle(
.textTheme
.titleSmall!
.copyWith(
color: replyItem!.isUp! color: replyItem!.isUp!
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Image.asset( Image.asset(
'assets/images/lv/lv${replyItem!.member!.level}.png', 'assets/images/lv/lv${replyItem!.member!.level}.png',
height: 13, height: 11,
), ),
const SizedBox(width: 6),
if (replyItem!.isUp!) UpTag()
], ],
), ),
], ],
@ -89,8 +85,6 @@ class ReplyItem extends StatelessWidget {
], ],
), ),
), ),
],
),
// title // title
Container( Container(
margin: const EdgeInsets.only(top: 0, left: 45, right: 6), margin: const EdgeInsets.only(top: 0, left: 45, right: 6),
@ -102,6 +96,8 @@ class ReplyItem extends StatelessWidget {
style: const TextStyle(height: 1.65), style: const TextStyle(height: 1.65),
TextSpan( TextSpan(
children: [ children: [
if (replyItem!.isTop!)
WidgetSpan(child: UpTag(tagText: '置顶')),
buildContent(context, replyItem!.content!), buildContent(context, replyItem!.content!),
], ],
), ),
@ -136,26 +132,9 @@ class ReplyItem extends StatelessWidget {
.labelMedium! .labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline), .copyWith(color: Theme.of(context).colorScheme.outline),
), ),
if (replyItem!.isTop!) ...[
Text(
' • 置顶',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
],
if (replyControl!.isUpTop!) ...[
Text(
' • 超赞',
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
// const SizedBox(width: 4),
],
const Spacer(), const Spacer(),
if (replyControl!.isUpTop!)
Icon(Icons.favorite, color: Colors.red[400], size: 18),
SizedBox( SizedBox(
height: 35, height: 35,
child: TextButton( child: TextButton(
@ -202,7 +181,7 @@ class ReplyItemRow extends StatelessWidget {
return Container( return Container(
margin: const EdgeInsets.only(left: 42, right: 4, top: 0), margin: const EdgeInsets.only(left: 42, right: 4, top: 0),
child: Material( child: Material(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.7), color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
animationDuration: Duration.zero, animationDuration: Duration.zero,
@ -245,21 +224,15 @@ class ReplyItemRow extends StatelessWidget {
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4), padding: EdgeInsets.fromLTRB(8, index == 0 ? 8 : 4, 8, 4),
child: Text.rich( child: Text.rich(
overflow: TextOverflow.ellipsis, overflow: extraRow == 1
maxLines: 2, ? TextOverflow.ellipsis
: TextOverflow.visible,
maxLines: extraRow == 1 ? 2 : null,
TextSpan( TextSpan(
children: [ children: [
if (replies![index].isUp) if (replies![index].isUp)
TextSpan( WidgetSpan(
text: 'UP • ', child: UpTag(),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context).colorScheme.primary,
),
), ),
TextSpan( TextSpan(
text: replies![index].member.uname + ' ', text: replies![index].member.uname + ' ',
@ -417,3 +390,31 @@ InlineSpan buildContent(BuildContext context, content) {
// spanChilds.add(TextSpan(text: matchMember)); // spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds); return TextSpan(children: spanChilds);
} }
class UpTag extends StatelessWidget {
String? tagText;
UpTag({super.key, this.tagText = 'UP'});
@override
Widget build(BuildContext context) {
return Container(
width: tagText == 'UP' ? 28 : 38,
height: tagText == 'UP' ? 17 : 19,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
// color: Theme.of(context).colorScheme.primary,
border: Border.all(color: Theme.of(context).colorScheme.primary)),
margin: const EdgeInsets.only(right: 4),
// padding: const EdgeInsets.symmetric(vertical: 0.5, horizontal: 4),
child: Center(
child: Text(
tagText!,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -19,6 +20,10 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
final double pinnedHeaderHeight = statusBarHeight +
kToolbarHeight +
MediaQuery.of(context).size.width * 9 / 16;
return DefaultTabController( return DefaultTabController(
initialIndex: videoDetailController.tabInitialIndex, initialIndex: videoDetailController.tabInitialIndex,
length: videoDetailController.tabs.length, // tab的数量. length: videoDetailController.tabs.length, // tab的数量.
@ -26,28 +31,21 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
top: false, top: false,
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
body: NestedScrollView( body: ExtendedNestedScrollView(
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) { (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverAppBar(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: const Text("视频详情"), title: const Text("视频详情"),
// floating: true,
// snap: true,
pinned: true, pinned: true,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
expandedHeight: MediaQuery.of(context).size.width * 9 / 16, expandedHeight: MediaQuery.of(context).size.width * 9 / 16,
collapsedHeight: MediaQuery.of(context).size.width * 9 / 16, collapsedHeight: MediaQuery.of(context).size.width * 9 / 16,
toolbarHeight: kToolbarHeight,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: Padding( background: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: 50,
top: MediaQuery.of(context).padding.top), top: MediaQuery.of(context).padding.top),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, boxConstraints) { builder: (context, boxConstraints) {
@ -66,16 +64,24 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
), ),
), ),
), ),
bottom: PreferredSize( ),
preferredSize: const Size.fromHeight(50.0), ];
child: Container( },
pinnedHeaderSliverHeightBuilder: () {
return pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [
Container(
height: 50, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: Theme.of(context) color: Theme.of(context).dividerColor.withOpacity(0.1),
.dividerColor ),
.withOpacity(0.1)))), ),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -104,48 +110,36 @@ class _VideoDetailPageState extends State<VideoDetailPage> {
], ],
), ),
), ),
), Expanded(
), child: TabBarView(
),
];
},
body: TabBarView(
children: [ children: [
Builder(builder: (context) { Builder(
builder: (context) {
return CustomScrollView( return CustomScrollView(
key: const PageStorageKey<String>('简介'), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[ slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
),
const VideoIntroPanel(), const VideoIntroPanel(),
SliverPadding( SliverPadding(
padding: const EdgeInsets.only(top: 8, bottom: 5), padding:
const EdgeInsets.only(top: 8, bottom: 5),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: Divider( child: Divider(
height: 1, height: 1,
color: color: Theme.of(context)
Theme.of(context).dividerColor.withOpacity(0.1), .dividerColor
.withOpacity(0.1),
), ),
), ),
), ),
const RelatedVideoPanel(), const RelatedVideoPanel(),
], ],
); );
}), },
Builder(builder: (context) {
return CustomScrollView(
key: const PageStorageKey<String>('评论'),
slivers: <Widget>[
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
), ),
const VideoReplyPanel() const VideoReplyPanel()
], ],
); ),
}) ),
], ],
), ),
), ),

View File

@ -153,6 +153,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.3" version: "1.6.3"
extended_nested_scroll_view:
dependency: "direct main"
description:
name: extended_nested_scroll_view
sha256: fc55b8f7e2c78701320d7eccda3b256387290b8498f0363d8ffd6f16760949d7
url: "https://pub.dev"
source: hosted
version: "6.0.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -509,6 +517,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d"
url: "https://pub.dev"
source: hosted
version: "0.3.3"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@ -535,4 +551,4 @@ packages:
version: "6.2.2" version: "6.2.2"
sdks: sdks:
dart: ">=2.19.6 <3.0.0" dart: ">=2.19.6 <3.0.0"
flutter: ">=3.4.0-17.0.pre" flutter: ">=3.7.0"

View File

@ -52,6 +52,8 @@ dependencies:
# 存储 # 存储
path_provider: ^2.0.14 path_provider: ^2.0.14
extended_nested_scroll_view: ^6.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter