feat: 动态主楼评论

This commit is contained in:
guozhigq
2023-09-12 18:54:56 +08:00
parent 7867af0f85
commit 838467451b
4 changed files with 281 additions and 170 deletions

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart';
@ -17,6 +18,7 @@ class DynamicDetailController extends GetxController {
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs;

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -9,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart';
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../widgets/dynamic_panel.dart';
@ -21,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
State<DynamicDetailPage> createState() => _DynamicDetailPageState();
}
class _DynamicDetailPageState extends State<DynamicDetailPage> {
late DynamicDetailController? _dynamicDetailController;
class _DynamicDetailPageState extends State<DynamicDetailPage>
with TickerProviderStateMixin {
late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title
final ScrollController scrollController = ScrollController();
late ScrollController scrollController;
bool _visibleTitle = false;
String? action;
// 回复类型
late int type;
bool _isFabVisible = true;
@override
void initState() {
@ -50,37 +57,30 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
}
} catch (_) {}
}
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
int commentType = 11;
try {
commentType = Get.arguments['item'].basic!['comment_type'];
} catch (_) {}
type = (commentType == 0) ? 11 : commentType;
action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController =
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
_futureBuilderFuture = _dynamicDetailController!.queryReplyList();
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
titleStreamC = StreamController<bool>();
scrollController.addListener(_listen);
if (action == 'comment') {
_visibleTitle = true;
titleStreamC.add(true);
}
}
void _listen() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
});
}
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
fabAnimationCtr.forward();
// 滚动事件监听
scrollListener();
}
void replyReply(replyItem) {
@ -107,9 +107,58 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
);
}
void scrollListener() {
scrollController = _dynamicDetailController.scrollController;
scrollController.addListener(
() {
// 分页加载
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
});
}
// 标题
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
// fab按钮
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
_hideFab();
}
},
);
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
fabAnimationCtr.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
fabAnimationCtr.reverse();
}
}
@override
void dispose() {
scrollController.removeListener(() {});
fabAnimationCtr.dispose();
scrollController.dispose();
super.dispose();
}
@ -136,15 +185,17 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
),
body: RefreshIndicator(
onRefresh: () async {
await _dynamicDetailController!.queryReplyList();
await _dynamicDetailController.queryReplyList();
},
child: CustomScrollView(
child: Stack(
children: [
CustomScrollView(
controller: scrollController,
slivers: [
if (action != 'comment')
SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController!.item,
item: _dynamicDetailController.item,
source: 'detail',
),
),
@ -156,7 +207,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context).dividerColor.withOpacity(0.05),
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
),
@ -173,9 +226,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
scale: animation, child: child);
},
child: Text(
'${_dynamicDetailController!.acount.value}',
'${_dynamicDetailController.acount.value}',
key: ValueKey<int>(
_dynamicDetailController!.acount.value),
_dynamicDetailController.acount.value),
),
),
),
@ -185,10 +238,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
height: 35,
child: TextButton.icon(
onPressed: () =>
_dynamicDetailController!.queryBySort(),
_dynamicDetailController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_dynamicDetailController!.sortTypeLabel.value,
_dynamicDetailController
.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
),
@ -207,11 +261,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _dynamicDetailController!.replyList.isEmpty &&
_dynamicDetailController!.isLoadingMore
() => _dynamicDetailController.replyList.isEmpty &&
_dynamicDetailController.isLoadingMore
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
)
@ -219,7 +273,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index ==
_dynamicDetailController!
_dynamicDetailController
.replyList.length) {
return Container(
padding: EdgeInsets.only(
@ -233,7 +287,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
child: Center(
child: Obx(
() => Text(
_dynamicDetailController!
_dynamicDetailController
.noMore.value,
style: TextStyle(
fontSize: 12,
@ -247,7 +301,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
);
} else {
return ReplyItem(
replyItem: _dynamicDetailController!
replyItem: _dynamicDetailController
.replyList[index],
showReplyRow: true,
replyLevel: '1',
@ -255,15 +309,15 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController!
_dynamicDetailController
.replyList[index].replies!
.add(replyItem);
},
);
}
},
childCount:
_dynamicDetailController!.replyList.length +
childCount: _dynamicDetailController
.replyList.length +
1,
),
),
@ -287,6 +341,52 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
)
],
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[type],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList
.add(value['data']),
_dynamicDetailController.acount.value++
}
},
);
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
),
],
),
),
);
}

View File

@ -62,6 +62,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
vsync: this, duration: const Duration(milliseconds: 300));
_futureBuilderFuture = _videoReplyController.queryReplyList();
fabAnimationCtr.forward();
scrollListener();
}
void scrollListener() {
scrollController = _videoReplyController.scrollController;
scrollController.addListener(
() {
@ -81,7 +87,6 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
}
},
);
fabAnimationCtr.forward();
}
void _showFab() {
@ -112,9 +117,10 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override
void dispose() {
super.dispose();
scrollController.removeListener(() {});
fabAnimationCtr.dispose();
scrollController.dispose();
super.dispose();
}
@override
@ -128,7 +134,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
child: Stack(
children: [
CustomScrollView(
controller: _videoReplyController.scrollController,
controller: scrollController,
key: const PageStorageKey<String>('评论'),
slivers: <Widget>[
SliverPersistentHeader(

View File

@ -744,11 +744,14 @@ InlineSpan buildContent(
recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
try {
Get.find<VideoDetailController>(
tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (_) {}
},
),
);