diff --git a/lib/pages/dynamics/deatil/controller.dart b/lib/pages/dynamics/deatil/controller.dart index 96ab65a6..62f0245d 100644 --- a/lib/pages/dynamics/deatil/controller.dart +++ b/lib/pages/dynamics/deatil/controller.dart @@ -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 replyList = [ReplyItemModel()].obs; RxInt acount = 0.obs; + final ScrollController scrollController = ScrollController(); ReplySortType _sortType = ReplySortType.time; RxString sortTypeTitle = ReplySortType.time.titles.obs; diff --git a/lib/pages/dynamics/deatil/view.dart b/lib/pages/dynamics/deatil/view.dart index b17eb78b..4c49ecc7 100644 --- a/lib/pages/dynamics/deatil/view.dart +++ b/lib/pages/dynamics/deatil/view.dart @@ -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 createState() => _DynamicDetailPageState(); } -class _DynamicDetailPageState extends State { - late DynamicDetailController? _dynamicDetailController; +class _DynamicDetailPageState extends State + with TickerProviderStateMixin { + late DynamicDetailController _dynamicDetailController; + late AnimationController fabAnimationCtr; Future? _futureBuilderFuture; late StreamController 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 { } } 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(); - 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 { ); } + 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,155 +185,206 @@ class _DynamicDetailPageState extends State { ), body: RefreshIndicator( onRefresh: () async { - await _dynamicDetailController!.queryReplyList(); + await _dynamicDetailController.queryReplyList(); }, - child: CustomScrollView( - controller: scrollController, - slivers: [ - if (action != 'comment') - SliverToBoxAdapter( - child: DynamicPanel( - item: _dynamicDetailController!.item, - source: 'detail', - ), - ), - SliverPersistentHeader( - delegate: _MySliverPersistentHeaderDelegate( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - width: 0.6, - color: Theme.of(context).dividerColor.withOpacity(0.05), - ), + child: Stack( + children: [ + CustomScrollView( + controller: scrollController, + slivers: [ + if (action != 'comment') + SliverToBoxAdapter( + child: DynamicPanel( + item: _dynamicDetailController.item, + source: 'detail', ), ), - height: 45, - padding: const EdgeInsets.only(left: 12, right: 6), - child: Row( - children: [ - Obx( - () => AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - transitionBuilder: - (Widget child, Animation animation) { - return ScaleTransition( - scale: animation, child: child); - }, - child: Text( - '${_dynamicDetailController!.acount.value}', - key: ValueKey( - _dynamicDetailController!.acount.value), + SliverPersistentHeader( + delegate: _MySliverPersistentHeaderDelegate( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + width: 0.6, + color: Theme.of(context) + .dividerColor + .withOpacity(0.05), ), ), ), - const Text('条回复'), - const Spacer(), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: () => - _dynamicDetailController!.queryBySort(), - icon: const Icon(Icons.sort, size: 16), - label: Obx(() => Text( - _dynamicDetailController!.sortTypeLabel.value, - style: const TextStyle(fontSize: 13), - )), - ), - ) - ], - ), - ), - ), - pinned: true, - ), - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (snapshot.data['status']) { - // 请求成功 - return Obx( - () => _dynamicDetailController!.replyList.isEmpty && - _dynamicDetailController!.isLoadingMore - ? SliverList( - delegate: - SliverChildBuilderDelegate((context, index) { - return const VideoReplySkeleton(); - }, childCount: 8), - ) - : SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == - _dynamicDetailController! - .replyList.length) { - return Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .padding - .bottom), - height: MediaQuery.of(context) - .padding - .bottom + - 100, - child: Center( - child: Obx( - () => Text( - _dynamicDetailController! - .noMore.value, - style: TextStyle( - fontSize: 12, - color: Theme.of(context) - .colorScheme - .outline, - ), - ), - ), - ), - ); - } else { - return ReplyItem( - replyItem: _dynamicDetailController! - .replyList[index], - showReplyRow: true, - replyLevel: '1', - replyReply: (replyItem) => - replyReply(replyItem), - replyType: ReplyType.values[type], - addReply: (replyItem) { - _dynamicDetailController! - .replyList[index].replies! - .add(replyItem); - }, - ); - } - }, - childCount: - _dynamicDetailController!.replyList.length + - 1, + height: 45, + padding: const EdgeInsets.only(left: 12, right: 6), + child: Row( + children: [ + Obx( + () => AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + transitionBuilder: + (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + '${_dynamicDetailController.acount.value}', + key: ValueKey( + _dynamicDetailController.acount.value), ), ), + ), + const Text('条回复'), + const Spacer(), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: () => + _dynamicDetailController.queryBySort(), + icon: const Icon(Icons.sort, size: 16), + label: Obx(() => Text( + _dynamicDetailController + .sortTypeLabel.value, + style: const TextStyle(fontSize: 13), + )), + ), + ) + ], + ), + ), + ), + pinned: true, + ), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (snapshot.data['status']) { + // 请求成功 + return Obx( + () => _dynamicDetailController.replyList.isEmpty && + _dynamicDetailController.isLoadingMore + ? SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoReplySkeleton(); + }, childCount: 8), + ) + : SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == + _dynamicDetailController + .replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .padding + .bottom), + height: MediaQuery.of(context) + .padding + .bottom + + 100, + child: Center( + child: Obx( + () => Text( + _dynamicDetailController + .noMore.value, + style: TextStyle( + fontSize: 12, + color: Theme.of(context) + .colorScheme + .outline, + ), + ), + ), + ), + ); + } else { + return ReplyItem( + replyItem: _dynamicDetailController + .replyList[index], + showReplyRow: true, + replyLevel: '1', + replyReply: (replyItem) => + replyReply(replyItem), + replyType: ReplyType.values[type], + addReply: (replyItem) { + _dynamicDetailController + .replyList[index].replies! + .add(replyItem); + }, + ); + } + }, + childCount: _dynamicDetailController + .replyList.length + + 1, + ), + ), + ); + } else { + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 8), + ); + } + }, + ) + ], + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 14, + right: 14, + child: SlideTransition( + position: Tween( + 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++ + } + }, ); - } else { - // 请求错误 - return HttpError( - errMsg: data['msg'], - fn: () => setState(() {}), - ); - } - } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoReplySkeleton(); - }, childCount: 8), - ); - } - }, - ) + }, + tooltip: '评论动态', + child: const Icon(Icons.reply), + ), + ), + ), ], ), ), diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 7b960014..d5469d92 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -62,6 +62,12 @@ class _VideoReplyPanelState extends State 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 } }, ); - fabAnimationCtr.forward(); } void _showFab() { @@ -112,9 +117,10 @@ class _VideoReplyPanelState extends State @override void dispose() { - super.dispose(); + scrollController.removeListener(() {}); fabAnimationCtr.dispose(); scrollController.dispose(); + super.dispose(); } @override @@ -128,7 +134,7 @@ class _VideoReplyPanelState extends State child: Stack( children: [ CustomScrollView( - controller: _videoReplyController.scrollController, + controller: scrollController, key: const PageStorageKey('评论'), slivers: [ SliverPersistentHeader( diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index fb06ca0a..76e63e72 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -744,11 +744,14 @@ InlineSpan buildContent( recognizer: TapGestureRecognizer() ..onTap = () { // 跳转到指定位置 - Get.find(tag: Get.arguments['heroTag']) - .plPlayerController - .seekTo( - Duration(seconds: Utils.duration(matchStr)), - ); + try { + Get.find( + tag: Get.arguments['heroTag']) + .plPlayerController + .seekTo( + Duration(seconds: Utils.duration(matchStr)), + ); + } catch (_) {} }, ), );