From f44b5793c21e0d951e0d9a3cab0f7c8e0ebf1de3 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sun, 17 Nov 2024 20:40:17 +0800 Subject: [PATCH 1/5] fix: repeated reply --- lib/pages/video/detail/reply/controller.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index 4af59b9d..38d59617 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -70,6 +70,13 @@ class VideoReplyController extends GetxController { isEnd = res['data'].cursor.isEnd ?? false; nextOffset = res['data'].cursor.paginationReply.nextOffset ?? ""; if (replies.isNotEmpty) { + /// 临时修复 + final bool flag = replyList + .any((ReplyItemModel reply) => reply.rpid == replies.first.rpid); + if (replies.length == 1 && flag) { + replies.clear(); + isEnd = true; + } noMore.value = isEnd ? '没有更多了' : '加载中...'; } else { noMore.value = From 4008b8caef561575f447d57c74d804fc731c3de1 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 20 Nov 2024 09:46:05 +0800 Subject: [PATCH 2/5] fix: controlsLock exit fullscreen --- lib/pages/video/detail/view.dart | 5 +++++ lib/plugin/pl_player/controller.dart | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 0fb97066..d357d3ea 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -684,6 +684,11 @@ class _VideoDetailPageState extends State canPop: plPlayerController?.isFullScreen.value != true, onPopInvoked: (bool didPop) { + if (plPlayerController?.controlsLock.value == + true) { + plPlayerController?.onLockControl(false); + return; + } if (plPlayerController?.isFullScreen.value == true) { plPlayerController! diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index de4cd9df..61711bf3 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -909,7 +909,7 @@ class PlPlayerController { if (videoType == 'live') { return; } - if (controlsLock.value) { + if (_controlsLock.value) { return; } _doubleSpeedStatus.value = val; @@ -1081,6 +1081,7 @@ class PlPlayerController { videoFitChangedTimer?.cancel(); // _position.close(); _playerEventSubs?.cancel(); + _controlsLock.value = false; // _sliderPosition.close(); // _sliderTempPosition.close(); // _isSliderMoving.close(); From 65c9027ef390422939a7b676b4b0768e9f37602b Mon Sep 17 00:00:00 2001 From: guozhigq Date: Thu, 28 Nov 2024 23:05:02 +0800 Subject: [PATCH 3/5] feat: marquee title --- .../video/detail/widgets/header_control.dart | 417 ++++++++++-------- pubspec.lock | 16 + pubspec.yaml | 2 + 3 files changed, 246 insertions(+), 189 deletions(-) diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 5412d326..28448f51 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -7,6 +7,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:marquee/marquee.dart'; import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/common/widgets/drag_handle.dart'; import 'package:pilipala/http/user.dart'; @@ -62,6 +63,7 @@ class _HeaderControlState extends State { late String heroTag; late VideoIntroController videoIntroController; late VideoDetailData videoDetail; + DateTime initialTime = DateTime.now(); @override void initState() { @@ -108,30 +110,6 @@ class _HeaderControlState extends State { child: Material( child: ListView( children: [ - // ListTile( - // onTap: () {}, - // dense: true, - // enabled: false, - // leading: - // const Icon(Icons.network_cell_outlined, size: 20), - // title: Text('省流模式', style: titleStyle), - // subtitle: Text('低画质 | 减少视频缓存', style: subTitleStyle), - // trailing: Transform.scale( - // scale: 0.75, - // child: Switch( - // thumbIcon: MaterialStateProperty.resolveWith( - // (Set states) { - // if (states.isNotEmpty && - // states.first == MaterialState.selected) { - // return const Icon(Icons.done); - // } - // return null; // All other states will use the default thumbIcon. - // }), - // value: false, - // onChanged: (value) => {}, - // ), - // ), - // ), ListTile( onTap: () async { final res = await UserHttp.toViewLater( @@ -1071,6 +1049,16 @@ class _HeaderControlState extends State { ); } + Stream _getTimeStream() { + return Stream.periodic(const Duration(seconds: 60), (count) { + return DateTime.now(); + }); + } + + String _formatTime(DateTime dateTime) { + return '${dateTime.hour}:${dateTime.minute < 10 ? '0${dateTime.minute}' : dateTime.minute}'; + } + @override Widget build(BuildContext context) { final _ = widget.controller!; @@ -1086,49 +1074,12 @@ class _HeaderControlState extends State { primary: false, automaticallyImplyLeading: false, titleSpacing: 14, - title: Row( + title: Column( children: [ - ComBtn( - icon: const Icon( - FontAwesomeIcons.arrowLeft, - size: 15, - color: Colors.white, - ), - fuc: () => >{ - if (widget.controller!.isFullScreen.value) - {widget.controller!.triggerFullScreen(status: false)} - else - { - if (MediaQuery.of(context).orientation == - Orientation.landscape) - { - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]) - }, - Get.back() - } - }, - ), - SizedBox(width: buttonSpace), - if (isFullScreen.value && - isLandscape && - widget.videoType == SearchType.video) ...[ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (isFullScreen.value && isLandscape) ...[ + Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: Obx( - () => Text( - videoIntroController.videoDetail.value.title ?? '', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - ), + const SizedBox(width: 40), if (videoIntroController.isShowOnlineTotal) Text( '${videoIntroController.total.value}人正在看', @@ -1136,138 +1087,226 @@ class _HeaderControlState extends State { color: Colors.white, fontSize: 12, ), - ) + ), + const Spacer(), + Expanded( + child: Align( + alignment: Alignment.center, + child: StreamBuilder( + stream: _getTimeStream(), + builder: (context, snapshot) { + if (snapshot.hasData) { + String currentTime = _formatTime(snapshot.data!); + return Text( + currentTime, + style: const TextStyle(fontSize: 12), + ); + } else if (snapshot.connectionState == + ConnectionState.waiting) { + // 如果Stream还未发出数据,先显示初始获取的时间 + String currentTime = _formatTime(initialTime); + return Text( + currentTime, + style: const TextStyle(fontSize: 12), + ); + } else { + return const SizedBox(); + } + }, + ), + ), + ), + const Spacer(), + + /// TODO 网络&电量 ], - ) - ] else ...[ - ComBtn( - icon: const Icon( - FontAwesomeIcons.house, - size: 15, - color: Colors.white, - ), - fuc: () async { - // 销毁播放器实例 - await widget.controller!.dispose(type: 'all'); - if (context.mounted) { - Navigator.popUntil( - context, (Route route) => route.isFirst); - } - }, ), ], - const Spacer(), - if (GlobalDataCache.enableDlna) ...[ - ComBtn( - icon: Image.asset('assets/images/video/dlna.png', width: 19), - fuc: () async { - showDialog( - context: context, - builder: (BuildContext context) { - return LiveDlnaPage( - datasource: widget.videoDetailCtr!.videoUrl); - }, - ); - }, - ), - SizedBox(width: buttonSpace), - ], - - /// 弹幕开关(全屏时) - if (isFullScreen.value) ...[ - SizedBox( - width: 56, - height: 34, - child: TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => showShootDanmakuSheet(), - child: const Text( - '发弹幕', - style: textStyle, - ), - ), - ), - SizedBox(width: buttonSpace), - SizedBox( - width: 34, - height: 34, - child: Obx( - () => IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - _.isOpenDanmu.value = !_.isOpenDanmu.value; - }, - icon: Icon( - _.isOpenDanmu.value - ? Icons.subtitles_outlined - : Icons.subtitles_off_outlined, - size: 19, - color: Colors.white, - ), - ), - ), - ), - SizedBox(width: buttonSpace), - ], - - /// pip - if (Platform.isAndroid) ...[ - SizedBox( - width: 34, - height: 34, - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () async { - bool canUsePiP = false; - widget.controller!.hiddenControls(false); - try { - canUsePiP = await widget.floating!.isPipAvailable; - } on PlatformException catch (_) { - canUsePiP = false; - } - if (canUsePiP) { - final Rational aspectRatio = Rational( - widget.videoDetailCtr!.data.dash!.video!.first.width!, - widget.videoDetailCtr!.data.dash!.video!.first.height!, - ); - await widget.floating!.enable(aspectRatio: aspectRatio); - } else {} - }, - icon: Image.asset( - 'assets/images/video/pip.png', - width: 19, + Row( + children: [ + ComBtn( + icon: const Icon( + FontAwesomeIcons.arrowLeft, + size: 15, color: Colors.white, ), + fuc: () => >{ + if (widget.controller!.isFullScreen.value) + {widget.controller!.triggerFullScreen(status: false)} + else + { + if (MediaQuery.of(context).orientation == + Orientation.landscape) + { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]) + }, + Get.back() + } + }, ), - ), - SizedBox(width: buttonSpace), - ], + SizedBox(width: buttonSpace), + if (isFullScreen.value && + isLandscape && + widget.videoType == SearchType.video) ...[ + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + return SizedBox( + width: constraints.maxWidth, + height: 25, + child: Obx( + () => Marquee( + text: videoIntroController.videoDetail.value.title ?? + '', + style: const TextStyle(fontSize: 16), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.center, + blankSpace: constraints.maxWidth, + velocity: 100, + pauseAfterRound: const Duration(seconds: 1), + startPadding: 0, + accelerationDuration: const Duration(seconds: 1), + accelerationCurve: Curves.linear, + decelerationDuration: const Duration(seconds: 1), + decelerationCurve: Curves.easeOut, + ), + ), + ); + }), + ), + ] else ...[ + ComBtn( + icon: const Icon( + FontAwesomeIcons.house, + size: 15, + color: Colors.white, + ), + fuc: () async { + // 销毁播放器实例 + await widget.controller!.dispose(type: 'all'); + if (context.mounted) { + Navigator.popUntil( + context, (Route route) => route.isFirst); + } + }, + ), + ], + const Spacer(), + if (GlobalDataCache.enableDlna) ...[ + ComBtn( + icon: Image.asset('assets/images/video/dlna.png', width: 19), + fuc: () async { + showDialog( + context: context, + builder: (BuildContext context) { + return LiveDlnaPage( + datasource: widget.videoDetailCtr!.videoUrl); + }, + ); + }, + ), + SizedBox(width: buttonSpace), + ], - /// 字幕 - if (widget.showSubtitleBtn) ...[ - ComBtn( - icon: Icon( - FontAwesomeIcons.closedCaptioning, - size: 16, - color: Colors.white.withOpacity(0.9), + /// 弹幕开关(全屏时) + if (isFullScreen.value) ...[ + SizedBox( + width: 56, + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => showShootDanmakuSheet(), + child: const Text( + '发弹幕', + style: textStyle, + ), + ), + ), + SizedBox(width: buttonSpace), + SizedBox( + width: 34, + height: 34, + child: Obx( + () => IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + _.isOpenDanmu.value = !_.isOpenDanmu.value; + }, + icon: Icon( + _.isOpenDanmu.value + ? Icons.subtitles_outlined + : Icons.subtitles_off_outlined, + size: 19, + color: Colors.white, + ), + ), + ), + ), + SizedBox(width: buttonSpace), + ], + + /// pip + if (Platform.isAndroid) ...[ + SizedBox( + width: 34, + height: 34, + child: IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () async { + bool canUsePiP = false; + widget.controller!.hiddenControls(false); + try { + canUsePiP = await widget.floating!.isPipAvailable; + } on PlatformException catch (_) { + canUsePiP = false; + } + if (canUsePiP) { + final Rational aspectRatio = Rational( + widget.videoDetailCtr!.data.dash!.video!.first.width!, + widget + .videoDetailCtr!.data.dash!.video!.first.height!, + ); + await widget.floating!.enable(aspectRatio: aspectRatio); + } else {} + }, + icon: Image.asset( + 'assets/images/video/pip.png', + width: 19, + color: Colors.white, + ), + ), + ), + SizedBox(width: buttonSpace), + ], + + /// 字幕 + if (widget.showSubtitleBtn) ...[ + ComBtn( + icon: Icon( + FontAwesomeIcons.closedCaptioning, + size: 16, + color: Colors.white.withOpacity(0.9), + ), + fuc: () => showSubtitleDialog(), + ), + SizedBox(width: buttonSpace), + ], + ComBtn( + icon: const Icon( + Icons.more_vert_outlined, + size: 19, + color: Colors.white, + ), + fuc: () => showSettingSheet(), ), - fuc: () => showSubtitleDialog(), - ), - SizedBox(width: buttonSpace), - ], - ComBtn( - icon: const Icon( - Icons.more_vert_outlined, - size: 19, - color: Colors.white, - ), - fuc: () => showSettingSheet(), + ], ), ], ), diff --git a/pubspec.lock b/pubspec.lock index a3dd3af9..715b23a5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -497,6 +497,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" + fading_edge_scrollview: + dependency: transitive + description: + name: fading_edge_scrollview + sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" fake_async: dependency: transitive description: @@ -990,6 +998,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" + marquee: + dependency: "direct main" + description: + name: marquee + sha256: "4b5243d2804373bdc25fc93d42c3b402d6ec1f4ee8d0bb72276edd04ae7addb8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b2275398..80a8b067 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -152,6 +152,8 @@ dependencies: re_highlight: ^0.0.3 # 图片选择器 image_picker: ^1.1.2 + # 跑马灯 + marquee: ^2.2.3 dev_dependencies: flutter_test: From b79a664e94e0444d5552475276e358e8cbd87965 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Fri, 29 Nov 2024 23:51:58 +0800 Subject: [PATCH 4/5] refactor: progressBar --- lib/plugin/pl_player/view.dart | 3 +- .../pl_player/widgets/bottom_control.dart | 50 +++--------------- .../pl_player/widgets/progress_bar.dart | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 lib/plugin/pl_player/widgets/progress_bar.dart diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index c379303e..b47d38de 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -842,8 +842,7 @@ class _PLVideoPlayerState extends State total: Duration(seconds: max), progressBarColor: colorTheme, baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: - Theme.of(context).colorScheme.primary.withOpacity(0.4), + bufferedBarColor: Colors.white.withOpacity(0.6), timeLabelLocation: TimeLabelLocation.none, thumbColor: colorTheme, barHeight: 3, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index b3ff37db..3c21c2af 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -1,8 +1,6 @@ -import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; -import 'package:pilipala/utils/feed_back.dart'; +import 'progress_bar.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; @@ -20,54 +18,18 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - Color colorTheme = Theme.of(context).colorScheme.primary; - final _ = controller!; return Container( color: Colors.transparent, height: 90, - padding: const EdgeInsets.only(left: 18, right: 18), + padding: const EdgeInsets.symmetric(horizontal: 18), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - Obx( - () { - final int value = _.sliderPositionSeconds.value; - final int max = _.durationSeconds.value; - final int buffer = _.bufferedSeconds.value; - if (value > max || max <= 0) { - return const SizedBox(); - } - return Padding( - padding: const EdgeInsets.only(left: 7, right: 7, bottom: 6), - child: ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), - progressBarColor: colorTheme, - baseBarColor: Colors.white.withOpacity(0.2), - bufferedBarColor: colorTheme.withOpacity(0.4), - timeLabelLocation: TimeLabelLocation.none, - thumbColor: colorTheme, - barHeight: 3.5, - thumbRadius: 7, - onDragStart: (duration) { - feedBack(); - _.onChangedSliderStart(); - }, - onDragUpdate: (duration) { - _.onUpdatedSliderProgress(duration.timeStamp); - }, - onSeek: (duration) { - _.onChangedSliderEnd(); - _.onChangedSlider(duration.inSeconds.toDouble()); - _.seekTo(Duration(seconds: duration.inSeconds), - type: 'slider'); - }, - ), - ); - }, + Padding( + padding: const EdgeInsets.fromLTRB(7, 0, 7, 6), + child: ProgressBarWidget(controller: controller!), ), - Row(children: [...buildBottomControl!]), + Row(children: buildBottomControl!), const SizedBox(height: 10), ], ), diff --git a/lib/plugin/pl_player/widgets/progress_bar.dart b/lib/plugin/pl_player/widgets/progress_bar.dart new file mode 100644 index 00000000..d0ed0a4e --- /dev/null +++ b/lib/plugin/pl_player/widgets/progress_bar.dart @@ -0,0 +1,52 @@ +import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/plugin/pl_player/index.dart'; +import 'package:pilipala/utils/feed_back.dart'; + +class ProgressBarWidget extends StatelessWidget { + final PlPlayerController controller; + + const ProgressBarWidget({ + required this.controller, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + Color colorTheme = Theme.of(context).colorScheme.primary; + final _ = controller; + final int value = _.sliderPositionSeconds.value; + final int max = _.durationSeconds.value; + final int buffer = _.bufferedSeconds.value; + if (value > max || max <= 0) { + return const SizedBox(); + } + return ProgressBar( + progress: Duration(seconds: value), + buffered: Duration(seconds: buffer), + total: Duration(seconds: max), + progressBarColor: colorTheme, + baseBarColor: Colors.white.withOpacity(0.2), + bufferedBarColor: Colors.white.withOpacity(0.6), + timeLabelLocation: TimeLabelLocation.none, + thumbColor: colorTheme, + barHeight: 3.5, + thumbRadius: 7, + onDragStart: (duration) { + feedBack(); + _.onChangedSliderStart(); + }, + onDragUpdate: (duration) { + _.onUpdatedSliderProgress(duration.timeStamp); + }, + onSeek: (duration) { + _.onChangedSliderEnd(); + _.onChangedSlider(duration.inSeconds.toDouble()); + _.seekTo(Duration(seconds: duration.inSeconds), type: 'slider'); + }, + ); + }); + } +} From 1127ef74370d7f5faef07ffb03718a9b585f94f6 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Sat, 30 Nov 2024 14:18:26 +0800 Subject: [PATCH 5/5] fix: _futureBuilderFuture init --- lib/pages/dynamics/detail/view.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index b1f9bea8..186a9e9a 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -20,7 +20,6 @@ import '../../../models/video/reply/item.dart'; import '../widgets/dynamic_panel.dart'; class DynamicDetailPage extends StatefulWidget { - // const DynamicDetailPage({super.key}); const DynamicDetailPage({Key? key}) : super(key: key); @override @@ -90,19 +89,22 @@ class _DynamicDetailPageState extends State _dynamicDetailController = Get.put( DynamicDetailController(oid, replyType), tag: opusId.toString()); + _futureBuilderFuture = _dynamicDetailController.queryReplyList(); await _dynamicDetailController.reqHtmlByOpusId(opusId!); setState(() {}); } } else { oid = moduleDynamic.major!.draw!.id!; } - } catch (_) {} + } catch (err) { + print('err:${err.toString()}'); + } } if (!isOpusId) { _dynamicDetailController = Get.put(DynamicDetailController(oid, replyType), tag: oid.toString()); + _futureBuilderFuture ??= _dynamicDetailController.queryReplyList(); } - _futureBuilderFuture = _dynamicDetailController.queryReplyList(); } // 查看二级评论 @@ -132,7 +134,7 @@ class _DynamicDetailPageState extends State // 分页加载 if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 300) { - EasyThrottle.throttle('replylist', const Duration(seconds: 2), () { + EasyThrottle.throttle('replyList', const Duration(seconds: 2), () { _dynamicDetailController.onLoad(); }); }