From 3776cfee35b33f5da35593b1525f9fc7fd6d8ac9 Mon Sep 17 00:00:00 2001 From: orz12 Date: Sat, 6 Apr 2024 00:06:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E9=80=89=E9=9B=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=92=AD=E6=94=BE=E5=99=A8=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=80=89=E9=9B=86=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=BF=9E?= =?UTF-8?q?=E6=92=AD=E9=80=80=E5=85=A8=E5=B1=8F=E3=80=81=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=B7=B2=E7=9C=8B=E5=AE=8C=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/list_sheet.dart | 169 +++++++ lib/models/video_detail_res.dart | 10 + .../bangumi/introduction/controller.dart | 9 +- lib/pages/bangumi/introduction/view.dart | 6 +- lib/pages/bangumi/widgets/bangumi_panel.dart | 178 ++----- .../video/detail/introduction/controller.dart | 5 +- lib/pages/video/detail/introduction/view.dart | 10 +- .../detail/introduction/widgets/page.dart | 161 ++----- .../detail/introduction/widgets/season.dart | 141 +----- lib/pages/video/detail/view.dart | 52 +- lib/plugin/pl_player/controller.dart | 16 +- .../pl_player/models/bottom_control_type.dart | 12 + lib/plugin/pl_player/view.dart | 453 ++++++++++++++---- .../pl_player/widgets/bottom_control.dart | 151 +----- lib/utils/utils.dart | 29 +- 15 files changed, 728 insertions(+), 674 deletions(-) create mode 100644 lib/common/widgets/list_sheet.dart create mode 100644 lib/plugin/pl_player/models/bottom_control_type.dart diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart new file mode 100644 index 00000000..862012c8 --- /dev/null +++ b/lib/common/widgets/list_sheet.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +import '../../utils/storage.dart'; +import '../../utils/utils.dart'; + +class ListSheet { + final dynamic episodes; + final String? bvid; + final int? aid; + final int currentCid; + final Function changeFucCall; + final BuildContext context; + PersistentBottomSheetController? bottomSheetController; + ListSheet({ + required this.episodes, + this.bvid, + this.aid, + required this.currentCid, + required this.changeFucCall, + required this.context, + }); + + Widget buildEpisodeListItem( + dynamic episode, + int index, + bool isCurrentIndex, + PersistentBottomSheetController bottomSheetController, + ) { + Color primary = Theme.of(context).colorScheme.primary; + late String title; + if (episode.runtimeType.toString() == "EpisodeItem") { + if (episode.longTitle != null && episode.longTitle != "") { + title = "第${(episode.title ?? '${index + 1}')}话 ${episode.longTitle!}"; + } else { + title = episode.title!; + } + } else if (episode.runtimeType.toString() == "PageItem") { + title = episode.pagePart!; + } else if (episode.runtimeType.toString() == "Part") { + title = episode.pagePart!; + // print("未知类型:${episode.runtimeType}"); + } + return ListTile( + onTap: () { + if (episode.badge != null && episode.badge == "会员") { + dynamic userInfo = GStrorage.userInfo.get('userInfoCache'); + int vipStatus = 0; + if (userInfo != null) { + vipStatus = userInfo.vipStatus; + } + if (vipStatus != 1) { + SmartDialog.showToast('需要大会员'); + return; + } + } + SmartDialog.showToast('切换到:$title'); + bottomSheetController.close(); + print(episode.runtimeType.toString()); + if (episode.runtimeType.toString() == "EpisodeItem") { + print(episode.bvid); + print(episode.cid); + print(episode.aid); + changeFucCall(episode.bvid, episode.cid, episode.aid); + } else { + changeFucCall(bvid!, episode.cid, aid!); + } + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.png', + color: primary, + height: 12, + semanticLabel: "正在播放:", + ) + : null, + title: Text( + title, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex + ? primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + trailing: episode.badge == null + ? null + : (episode.badge == '会员' + ? Image.asset( + 'assets/images/big-vip.png', + height: 20, + semanticLabel: "大会员", + ) + : Text(episode.badge)), + ); + } + + void buildShowBottomSheet() { + int currentIndex = + episodes!.indexWhere((dynamic e) => e.cid == currentCid) ?? 0; + final ItemScrollController itemScrollController = ItemScrollController(); + bottomSheetController = showBottomSheet( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + itemScrollController.jumpTo(index: currentIndex); + }); + return Container( + height: Utils.getSheetHeight(context), + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + Container( + height: 45, + padding: const EdgeInsets.only(left: 14, right: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '合集(${episodes!.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + IconButton( + tooltip: '关闭', + icon: const Icon(Icons.close), + onPressed: () => bottomSheetController!.close(), + ), + ], + ), + ), + Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + Expanded( + child: Material( + child: ScrollablePositionedList.builder( + itemCount: episodes!.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = index == episodes!.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: + MediaQuery.of(context).padding.bottom + 20, + ) + : buildEpisodeListItem( + episodes![index], + index, + isCurrentIndex, + bottomSheetController!, + ); + }, + itemScrollController: itemScrollController, + ), + ), + ), + ], + ), + ); + }); + }, + ); + } +} diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index 8ec1e201..147cdac2 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -378,6 +378,7 @@ class Part { String? weblink; Dimension? dimension; String? firstFrame; + String? badge; Part({ this.cid, @@ -389,6 +390,7 @@ class Part { this.weblink, this.dimension, this.firstFrame, + this.badge, }); fromRawJson(String str) => Part.fromJson(json.decode(str)); @@ -407,6 +409,7 @@ class Part { ? null : Dimension.fromJson(json["dimension"]); firstFrame = json["first_frame"]; + badge = json["badge"]; } Map toJson() { @@ -420,6 +423,7 @@ class Part { data["weblink"] = weblink; data["dimension"] = dimension?.toJson(); data["first_frame"] = firstFrame; + data["badge"] = badge; return data; } } @@ -627,9 +631,11 @@ class EpisodeItem { this.aid, this.cid, this.title, + this.longTitle, this.attribute, this.page, this.bvid, + this.badge, }); int? seasonId; int? sectionId; @@ -637,9 +643,11 @@ class EpisodeItem { int? aid; int? cid; String? title; + String? longTitle; int? attribute; Part? page; String? bvid; + String? badge; EpisodeItem.fromJson(Map json) { seasonId = json['season_id']; @@ -648,8 +656,10 @@ class EpisodeItem { aid = json['aid']; cid = json['cid']; title = json['title']; + longTitle = json['long_title']; attribute = json['attribute']; page = Part.fromJson(json['page']); bvid = json['bvid']; + badge = json['badge']; } } diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index f79d9ade..2ebf452d 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -295,7 +295,7 @@ class BangumiIntroController extends GetxController { } /// 列表循环或者顺序播放时,自动播放下一个 - void nextPlay() { + bool nextPlay() { late List episodes; if (bangumiDetail.value.episodes != null) { episodes = bangumiDetail.value.episodes!; @@ -312,12 +312,15 @@ class BangumiIntroController extends GetxController { nextIndex = 0; } } - if (nextIndex <= episodes.length - 1 && - platRepeat == PlayRepeat.listOrder) {} + if (nextIndex == episodes.length - 1 && + platRepeat == PlayRepeat.listOrder) { + return false; + } int cid = episodes[nextIndex].cid!; String bvid = episodes[nextIndex].bvid!; int aid = episodes[nextIndex].aid!; changeSeasonOrbangu(bvid, cid, aid); + return true; } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index dd79149c..162aff90 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -56,6 +56,7 @@ class _BangumiIntroPanelState extends State _futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); videoDetailCtr.cid.listen((int p0) { cid = p0; + if (!mounted) return; setState(() {}); }); } @@ -138,7 +139,7 @@ class _BangumiInfoState extends State { print('cid: $cid'); videoDetailCtr.cid.listen((p0) { cid = p0; - print('cid: $cid'); + if (!mounted) return; setState(() {}); }); } @@ -371,8 +372,7 @@ class _BangumiInfoState extends State { (bangumiItem != null ? bangumiItem!.episodes!.first.cid : widget.bangumiDetail!.episodes!.first.cid), - changeFuc: (bvid, cid, aid) => bangumiIntroController - .changeSeasonOrbangu(bvid, cid, aid), + changeFuc: bangumiIntroController.changeSeasonOrbangu, ) ], ], diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 478bdf0b..6b09654c 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -6,20 +6,19 @@ import 'package:PiliPalaX/models/bangumi/info.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -import '../../../utils/utils.dart'; +import 'package:PiliPalaX/common/widgets/list_sheet.dart'; class BangumiPanel extends StatefulWidget { const BangumiPanel({ super.key, required this.pages, this.cid, - this.changeFuc, + required this.changeFuc, }); final List pages; final int? cid; - final Function? changeFuc; + final Function changeFuc; @override State createState() => _BangumiPanelState(); @@ -52,8 +51,9 @@ class _BangumiPanelState extends State { videoDetailCtr.cid.listen((int p0) { cid = p0; - setState(() {}); currentIndex = widget.pages.indexWhere((EpisodeItem e) => e.cid == cid); + if (!mounted) return; + setState(() {}); scrollToIndex(); }); } @@ -65,131 +65,20 @@ class _BangumiPanelState extends State { super.dispose(); } - Widget buildPageListItem( - EpisodeItem page, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () { - Get.back(); - setState(() { - changeFucCall(page, index); - }); - }, - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.png', - color: primary, - height: 12, - semanticLabel: "正在播放:", - ) - : null, - title: Text( - '第' + (page.title ?? '${index + 1}') + '话 ${page.longTitle!}', - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - trailing: page.badge != null && page.badge == '会员' - ? Image.asset( - 'assets/images/big-vip.png', - height: 20, - semanticLabel: "大会员", - ) - : Text(page.badge ?? ""), - ); - } - - void showBangumiPanel() { - showBottomSheet( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - // await Future.delayed(const Duration(milliseconds: 200)); - // listViewScrollCtr_2.animateTo(currentIndex * 56, - // duration: const Duration(milliseconds: 500), - // curve: Curves.easeInOut); - itemScrollController.jumpTo(index: currentIndex); - }); - // 在这里使用 setState 更新状态 - return Container( - height: Utils.getSheetHeight(context), - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - AppBar( - toolbarHeight: 45, - automaticallyImplyLeading: false, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${widget.pages.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - tooltip: '关闭', - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - titleSpacing: 10, - ), - Expanded( - child: Material( - child: ScrollablePositionedList.builder( - itemCount: widget.pages.length + 1, - itemBuilder: (BuildContext context, int index) { - bool isLastItem = index == widget.pages.length; - bool isCurrentIndex = currentIndex == index; - return isLastItem - ? SizedBox( - height: - MediaQuery.of(context).padding.bottom + - 50, - ) - : buildPageListItem( - widget.pages[index], - index, - isCurrentIndex, - ); - }, - itemScrollController: itemScrollController, - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } - - void changeFucCall(item, i) async { - if (item.badge != null && item.badge == '会员' && vipStatus != 1) { - SmartDialog.showToast('需要大会员'); - return; - } - await widget.changeFuc!( - item.bvid, - item.cid, - item.aid, - ); - currentIndex = i; - setState(() {}); - scrollToIndex(); - } + // void changeFucCall(item, i) async { + // if (item.badge != null && item.badge == '会员' && vipStatus != 1) { + // SmartDialog.showToast('需要大会员'); + // return; + // } + // await widget.changeFuc!( + // item.bvid, + // item.cid, + // item.aid, + // ); + // currentIndex = i; + // setState(() {}); + // scrollToIndex(); + // } void scrollToIndex() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -226,7 +115,16 @@ class _BangumiPanelState extends State { style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), ), - onPressed: () => showBangumiPanel(), + onPressed: () { + ListSheet( + episodes: widget.pages, + bvid: widget.pages[currentIndex].bvid!, + aid: widget.pages[currentIndex].aid!, + currentCid: cid, + changeFucCall: widget.changeFuc, + context: context) + .buildShowBottomSheet(); + }, child: Text( '全${widget.pages.length}话', style: const TextStyle(fontSize: 13), @@ -252,7 +150,23 @@ class _BangumiPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => changeFucCall(widget.pages[i], i), + onTap: () { + if (widget.pages[i].badge != null && + widget.pages[i].badge == '会员' && + vipStatus != 1) { + SmartDialog.showToast('需要大会员'); + return; + } + widget.changeFuc( + widget.pages[i].bvid, + widget.pages[i].cid, + widget.pages[i].aid, + ); + // currentIndex = i; + // setState(() {}); + // scrollToIndex(); + }, + //changeFucCall(widget.pages[i], i), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 10), diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 4a05e5f2..f632b43f 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -532,7 +532,7 @@ class VideoIntroController extends GetxController { } /// 列表循环或者顺序播放时,自动播放下一个 - void nextPlay() { + bool nextPlay() { final List episodes = []; bool isPages = false; if (videoDetail.value.ugcSeason != null) { @@ -561,13 +561,14 @@ class VideoIntroController extends GetxController { nextIndex = 0; } if (platRepeat == PlayRepeat.listOrder) { - return; + return false; } } final int cid = episodes[nextIndex].cid!; final String rBvid = isPages ? bvid : episodes[nextIndex].bvid; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!; changeSeasonOrbangu(rBvid, cid, rAid); + return true; } // 设置关注分组 diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index f23a8f46..e86eed6f 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -374,8 +374,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { cid: videoIntroController.lastPlayCid.value != 0 ? videoIntroController.lastPlayCid.value : widget.videoDetail!.pages!.first.cid, - changeFuc: (bvid, cid, aid) => videoIntroController - .changeSeasonOrbangu(bvid, cid, aid), + changeFuc: videoIntroController.changeSeasonOrbangu, ), ) ], @@ -385,11 +384,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { Obx(() => PagesPanel( pages: widget.videoDetail!.pages!, cid: videoIntroController.lastPlayCid.value, - changeFuc: (cid) => - videoIntroController.changeSeasonOrbangu( - videoIntroController.bvid, - cid, - IdUtils.bv2av(videoIntroController.bvid)), + bvid: videoIntroController.bvid, + changeFuc: videoIntroController.changeSeasonOrbangu, )) ], GestureDetector( diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart index ccc6d1cd..4bd03ad2 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -1,22 +1,25 @@ import 'dart:math'; +import 'package:PiliPalaX/common/widgets/list_sheet.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/models/video_detail_res.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; -import '../../../../../utils/utils.dart'; +import '../../../../../utils/id_utils.dart'; class PagesPanel extends StatefulWidget { const PagesPanel({ super.key, required this.pages, this.cid, - this.changeFuc, + required this.bvid, + required this.changeFuc, }); final List pages; final int? cid; - final Function? changeFuc; + final String bvid; + final Function changeFuc; @override State createState() => _PagesPanelState(); @@ -28,7 +31,6 @@ class _PagesPanelState extends State { late int currentIndex; final String heroTag = Get.arguments['heroTag']; late VideoDetailController _videoDetailController; - final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController2 = ScrollController(); @override @@ -40,66 +42,27 @@ class _PagesPanelState extends State { currentIndex = episodes.indexWhere((Part e) => e.cid == cid); _videoDetailController.cid.listen((int p0) { cid = p0; - setState(() {}); currentIndex = episodes.indexWhere((Part e) => e.cid == cid); + if (!mounted) return; + const double itemWidth = 150; // 每个列表项的宽度 + final double targetOffset = min( + (currentIndex * itemWidth) - (itemWidth / 2), + _scrollController2.position.maxScrollExtent); + // 滑动至目标位置 + _scrollController2.animateTo( + targetOffset, + duration: const Duration(milliseconds: 300), // 滑动动画持续时间 + curve: Curves.easeInOut, // 滑动动画曲线 + ); }); } - void changeFucCall(item, i) async { - await widget.changeFuc!(item.cid); - - const double itemWidth = 150; // 每个列表项的宽度 - final double targetOffset = min((i * itemWidth) - (itemWidth / 2), - _scrollController2.position.maxScrollExtent); - - // 滑动至目标位置 - _scrollController2.animateTo( - targetOffset, - duration: const Duration(milliseconds: 300), // 滑动动画持续时间 - curve: Curves.easeInOut, // 滑动动画曲线 - ); - currentIndex = i; - setState(() {}); - } - @override void dispose() { - _scrollController.dispose(); + _scrollController2.dispose(); super.dispose(); } - Widget buildEpisodeListItem( - Part episode, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () { - changeFucCall(episode, index); - Get.back(); - }, - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.png', - color: primary, - height: 12, - semanticLabel: "正在播放:", - ) - : null, - title: Text( - episode.pagePart!, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - @override Widget build(BuildContext context) { return Column( @@ -128,83 +91,14 @@ class _PagesPanelState extends State { padding: MaterialStateProperty.all(EdgeInsets.zero), ), onPressed: () { - showBottomSheet( + ListSheet( + episodes: episodes, + bvid: widget.bvid, + aid: IdUtils.bv2av(widget.bvid), + currentCid: cid, + changeFucCall: widget.changeFuc, context: context, - builder: (BuildContext context) { - return StatefulBuilder(builder: - (BuildContext context, StateSetter setState) { - WidgetsBinding.instance - .addPostFrameCallback((_) async { - await Future.delayed( - const Duration(milliseconds: 200)); - _scrollController.jumpTo(currentIndex * 56); - }); - return Container( - height: Utils.getSheetHeight(context), - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - Container( - height: 45, - padding: const EdgeInsets.only( - left: 14, right: 14), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${episodes.length})', - style: Theme.of(context) - .textTheme - .titleMedium, - ), - IconButton( - tooltip: '关闭', - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - Divider( - height: 1, - color: Theme.of(context) - .dividerColor - .withOpacity(0.1), - ), - Expanded( - child: Material( - child: ListView.builder( - controller: _scrollController, - itemCount: episodes.length + 1, - itemBuilder: - (BuildContext context, int index) { - bool isLastItem = - index == episodes.length; - bool isCurrentIndex = - currentIndex == index; - return isLastItem - ? SizedBox( - height: MediaQuery.of(context) - .padding - .bottom + - 20, - ) - : buildEpisodeListItem( - episodes[index], - index, - isCurrentIndex, - ); - }, - ), - ), - ), - ], - ), - ); - }); - }, - ); + ).buildShowBottomSheet(); }, child: Text( '共${widget.pages.length}集', @@ -233,7 +127,10 @@ class _PagesPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => changeFucCall(widget.pages[i], i), + onTap: () => { + widget.changeFuc(widget.bvid, widget.pages[i].cid, + IdUtils.bv2av(widget.bvid)) + }, child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 8), diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 5223d0a3..390c50aa 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -1,23 +1,20 @@ - +import 'package:PiliPalaX/common/widgets/list_sheet.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/models/video_detail_res.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; import 'package:PiliPalaX/utils/id_utils.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -import '../../../../../utils/utils.dart'; class SeasonPanel extends StatefulWidget { const SeasonPanel({ super.key, required this.ugcSeason, this.cid, - this.changeFuc, + required this.changeFuc, }); final UgcSeason ugcSeason; final int? cid; - final Function? changeFuc; + final Function changeFuc; @override State createState() => _SeasonPanelState(); @@ -30,7 +27,6 @@ class _SeasonPanelState extends State { final String heroTag = Get.arguments['heroTag']; late VideoDetailController _videoDetailController; final ScrollController _scrollController = ScrollController(); - final ItemScrollController itemScrollController = ItemScrollController(); @override void initState() { @@ -62,21 +58,22 @@ class _SeasonPanelState extends State { currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); _videoDetailController.cid.listen((int p0) { cid = p0; - setState(() {}); currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); + if (!mounted) return; + setState(() {}); }); } - void changeFucCall(item, int i) async { - await widget.changeFuc!( - IdUtils.av2bv(item.aid), - item.cid, - item.aid, - ); - currentIndex = i; - Get.back(); - setState(() {}); - } + // void changeFucCall(item, int i) async { + // await widget.changeFuc!( + // IdUtils.av2bv(item.aid), + // item.cid, + // item.aid, + // ); + // currentIndex = i; + // Get.back(); + // setState(() {}); + // } @override void dispose() { @@ -84,35 +81,6 @@ class _SeasonPanelState extends State { super.dispose(); } - Widget buildEpisodeListItem( - EpisodeItem episode, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () => changeFucCall(episode, index), - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.png', - color: primary, - height: 12, - semanticLabel: "正在播放:", - ) - : null, - title: Text( - episode.title!, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - @override Widget build(BuildContext context) { if (episodes == null) { @@ -131,75 +99,16 @@ class _SeasonPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => showBottomSheet( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - itemScrollController.jumpTo(index: currentIndex); - }); - return Container( - height: Utils.getSheetHeight(context), - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - Container( - height: 45, - padding: const EdgeInsets.only(left: 14, right: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${episodes!.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - tooltip: '关闭', - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - Divider( - height: 1, - color: - Theme.of(context).dividerColor.withOpacity(0.1), - ), - Expanded( - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom), - child: Material( - child: ScrollablePositionedList.builder( - itemCount: episodes!.length + 1, - itemBuilder: (BuildContext context, int index) { - bool isLastItem = index == episodes!.length; - bool isCurrentIndex = currentIndex == index; - return isLastItem - ? SizedBox( - height: MediaQuery.of(context) - .padding - .bottom + - 20, - ) - : buildEpisodeListItem( - episodes![index], - index, - isCurrentIndex, - ); - }, - itemScrollController: itemScrollController, - ), - ), - )), - ], - ), - ); - }); - }, - ), + onTap: () { + ListSheet( + episodes: episodes, + // bvid: IdUtils.av2bv(episodes!.first.aid!), + // aid: episodes!.first.aid!, + currentCid: cid, + changeFucCall: widget.changeFuc, + context: context) + .buildShowBottomSheet(); + }, child: Padding( padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), child: Row( diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index a432fceb..c385e063 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -146,28 +146,30 @@ class _VideoDetailPageState extends State void playerListener(PlayerStatus? status) async { playerStatus = status!; if (status == PlayerStatus.completed) { - // 结束播放退出全屏 - if (autoExitFullcreen) { - plPlayerController!.triggerFullScreen(status: false); - } shutdownTimerService.handleWaitingFinished(); - + bool notExitFlag = false; /// 顺序播放 列表循环 if (plPlayerController!.playRepeat != PlayRepeat.pause && plPlayerController!.playRepeat != PlayRepeat.singleCycle) { if (videoDetailController.videoType == SearchType.video) { - videoIntroController.nextPlay(); + notExitFlag = videoIntroController.nextPlay(); } if (videoDetailController.videoType == SearchType.media_bangumi) { - bangumiIntroController.nextPlay(); + notExitFlag = bangumiIntroController.nextPlay(); } } /// 单个循环 if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) { + notExitFlag = true; plPlayerController!.seekTo(Duration.zero); plPlayerController!.play(); } + + // 结束播放退出全屏 + if (!notExitFlag && autoExitFullcreen) { + plPlayerController!.triggerFullScreen(status: false); + } // 播放完展示控制栏 if (videoDetailController.floating != null) { PiPStatus currentStatus = @@ -263,6 +265,7 @@ class _VideoDetailPageState extends State // 离开当前页面时 void didPushNext() async { _bufferedListener?.cancel(); + /// 开启 if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false) as bool) { @@ -449,6 +452,19 @@ class _VideoDetailPageState extends State : PLVideoPlayer( controller: plPlayerController!, + videoIntroController: + videoDetailController + .videoType == + SearchType.video + ? videoIntroController + : null, + bangumiIntroController: + videoDetailController + .videoType == + SearchType + .media_bangumi + ? bangumiIntroController + : null, headerControl: videoDetailController .headerControl, @@ -702,6 +718,20 @@ class _VideoDetailPageState extends State : PLVideoPlayer( controller: plPlayerController!, + videoIntroController: + videoDetailController + .videoType == + SearchType + .video + ? videoIntroController + : null, + bangumiIntroController: + videoDetailController + .videoType == + SearchType + .media_bangumi + ? bangumiIntroController + : null, headerControl: videoDetailController .headerControl, @@ -875,6 +905,14 @@ class _VideoDetailPageState extends State ? const SizedBox() : PLVideoPlayer( controller: plPlayerController!, + videoIntroController: + videoDetailController.videoType == SearchType.video + ? videoIntroController + : null, + bangumiIntroController: + videoDetailController.videoType == SearchType.media_bangumi + ? bangumiIntroController + : null, headerControl: HeaderControl( controller: plPlayerController, videoDetailCtr: videoDetailController, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 705e5c1c..b5d991ce 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -85,6 +85,7 @@ class PlPlayerController { final Rx _videoFitDesc = Rx(videoFitType.first['desc']); late StreamSubscription _dataListenerForVideoFit; late StreamSubscription _dataListenerForEnterFullscreen; + /// 后台播放 final Rx _backgroundPlay = false.obs; @@ -126,6 +127,9 @@ class PlPlayerController { PreferredSizeWidget? bottomControl; Widget? danmuWidget; + String get bvid => _bvid; + int get cid => _cid; + /// 数据加载监听 Stream get onDataStatusChanged => dataStatus.status.stream; @@ -620,7 +624,7 @@ class PlPlayerController { } else { // playerStatus.status.value = PlayerStatus.playing; } - makeHeartBeat(positionSeconds.value, type: 'status'); + makeHeartBeat(positionSeconds.value, type: 'completed'); }), videoPlayerController!.stream.position.listen((event) { _position.value = event; @@ -1100,15 +1104,17 @@ class PlPlayerController { if (videoType.value == 'live') { return; } + bool isComplete = playerStatus.status.value == PlayerStatus.completed || + type == 'completed'; // 播放状态变化时,更新 - if (type == 'status') { + if (type == 'status' || type == 'completed') { await VideoHttp.heartBeat( bvid: _bvid, cid: _cid, - progress: - playerStatus.status.value == PlayerStatus.completed ? -1 : progress, + progress: isComplete ? -1 : progress, ); - } else + return; + } // 正常播放时,间隔5秒更新一次 if (progress - _heartDuration >= 5) { _heartDuration = progress; diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart new file mode 100644 index 00000000..0d582d19 --- /dev/null +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -0,0 +1,12 @@ +enum BottomControlType { + pre, + playOrPause, + next, + time, + space, + episode, + fit, + subtitle, + speed, + fullscreen, +} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 9960e6c6..2720fbb7 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'dart:ui'; +import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; +import 'package:PiliPalaX/utils/id_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; @@ -8,37 +11,48 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; -import 'package:nil/nil.dart'; import 'package:PiliPalaX/plugin/pl_player/controller.dart'; import 'package:PiliPalaX/plugin/pl_player/models/duration.dart'; import 'package:PiliPalaX/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPalaX/plugin/pl_player/utils.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/storage.dart'; +import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart'; import 'package:screen_brightness/screen_brightness.dart'; import '../../common/widgets/audio_video_progress_bar.dart'; +import '../../models/video_detail_res.dart'; +import '../../pages/bangumi/introduction/controller.dart'; +import '../../common/widgets/list_sheet.dart'; import '../../utils/utils.dart'; +import 'models/bottom_control_type.dart'; import 'models/bottom_progress_behavior.dart'; import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; import 'widgets/bottom_control.dart'; import 'widgets/common_btn.dart'; import 'widgets/forward_seek.dart'; +import 'widgets/play_pause_btn.dart'; class PLVideoPlayer extends StatefulWidget { const PLVideoPlayer({ required this.controller, + this.videoIntroController, + this.bangumiIntroController, this.headerControl, this.bottomControl, this.danmuWidget, + this.bottomList, super.key, }); final PlPlayerController controller; + final VideoIntroController? videoIntroController; + final BangumiIntroController? bangumiIntroController; final PreferredSizeWidget? headerControl; final PreferredSizeWidget? bottomControl; final Widget? danmuWidget; + final List? bottomList; @override State createState() => _PLVideoPlayerState(); @@ -48,27 +62,25 @@ class _PLVideoPlayerState extends State with TickerProviderStateMixin { late AnimationController animationController; late VideoController videoController; - final PLVideoPlayerController _ctr = Get.put(PLVideoPlayerController()); + late VideoIntroController? videoIntroController; + late BangumiIntroController? bangumiIntroController; final GlobalKey _playerKey = GlobalKey(); - // bool _mountSeekBackwardButton = false; - // bool _mountSeekForwardButton = false; - // bool _hideSeekBackwardButton = false; - // bool _hideSeekForwardButton = false; + final RxBool _mountSeekBackwardButton = false.obs; + final RxBool _mountSeekForwardButton = false.obs; + final RxBool _hideSeekBackwardButton = false.obs; + final RxBool _hideSeekForwardButton = false.obs; - // double _brightnessValue = 0.0; - // bool _brightnessIndicator = false; + final RxDouble _brightnessValue = 0.0.obs; + final RxBool _brightnessIndicator = false.obs; Timer? _brightnessTimer; - // double _volumeValue = 0.0; - // bool _volumeIndicator = false; + final RxDouble _volumeValue = 0.0.obs; + final RxBool _volumeIndicator = false.obs; Timer? _volumeTimer; - double _distance = 0.0; - // 初始手指落下位置 - // double _initTapPositoin = 0.0; - - // bool _volumeInterceptEventStream = false; + final RxDouble _distance = 0.0.obs; + final RxBool _volumeInterceptEventStream = false.obs; Box setting = GStrorage.setting; late FullScreenMode mode; @@ -87,11 +99,11 @@ class _PLVideoPlayerState extends State double _lastAnnouncedValue = -1; void onDoubleTapSeekBackward() { - _ctr.onDoubleTapSeekBackward(); + _mountSeekBackwardButton.value = true; } void onDoubleTapSeekForward() { - _ctr.onDoubleTapSeekForward(); + _mountSeekForwardButton.value = true; } // 双击播放、暂停 @@ -126,6 +138,8 @@ class _PLVideoPlayerState extends State animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100)); videoController = widget.controller.videoController!; + videoIntroController = widget.videoIntroController; + bangumiIntroController = widget.bangumiIntroController; widget.controller.headerControl = widget.headerControl; widget.controller.bottomControl = widget.bottomControl; widget.controller.danmuWidget = widget.danmuWidget; @@ -138,10 +152,10 @@ class _PLVideoPlayerState extends State Future.microtask(() async { try { FlutterVolumeController.updateShowSystemUI(true); - _ctr.volumeValue.value = (await FlutterVolumeController.getVolume())!; + _volumeValue.value = (await FlutterVolumeController.getVolume())!; FlutterVolumeController.addListener((double value) { - if (mounted && !_ctr.volumeInterceptEventStream.value) { - _ctr.volumeValue.value = value; + if (mounted && !_volumeInterceptEventStream.value) { + _volumeValue.value = value; } }); } catch (_) {} @@ -149,10 +163,10 @@ class _PLVideoPlayerState extends State Future.microtask(() async { try { - _ctr.brightnessValue.value = await ScreenBrightness().current; + _brightnessValue.value = await ScreenBrightness().current; ScreenBrightness().onCurrentBrightnessChanged.listen((double value) { if (mounted) { - _ctr.brightnessValue.value = value; + _brightnessValue.value = value; } }); } catch (_) {} @@ -164,14 +178,14 @@ class _PLVideoPlayerState extends State FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(value); } catch (_) {} - _ctr.volumeValue.value = value; - _ctr.volumeIndicator.value = true; - _ctr.volumeInterceptEventStream.value = true; + _volumeValue.value = value; + _volumeIndicator.value = true; + _volumeInterceptEventStream.value = true; _volumeTimer?.cancel(); _volumeTimer = Timer(const Duration(milliseconds: 200), () { if (mounted) { - _ctr.volumeIndicator.value = false; - _ctr.volumeInterceptEventStream.value = false; + _volumeIndicator.value = false; + _volumeInterceptEventStream.value = false; } }); } @@ -180,11 +194,11 @@ class _PLVideoPlayerState extends State try { await ScreenBrightness().setScreenBrightness(value); } catch (_) {} - _ctr.brightnessIndicator.value = true; + _brightnessIndicator.value = true; _brightnessTimer?.cancel(); _brightnessTimer = Timer(const Duration(milliseconds: 200), () { if (mounted) { - _ctr.brightnessIndicator.value = false; + _brightnessIndicator.value = false; } }); widget.controller.brightness.value = value; @@ -197,6 +211,268 @@ class _PLVideoPlayerState extends State super.dispose(); } + // 动态构建底部控制条 + List buildBottomControl() { + final PlPlayerController _ = widget.controller; + Map videoProgressWidgets = { + /// 上一集 + BottomControlType.pre: ComBtn( + icon: const Icon( + Icons.skip_previous, + size: 15, + color: Colors.white, + ), + fuc: () {}, + tooltip: '上一集', + ), + + /// 播放暂停 + BottomControlType.playOrPause: PlayOrPauseButton( + controller: _, + ), + + /// 下一集 + BottomControlType.next: ComBtn( + icon: const Icon( + Icons.skip_next, + size: 15, + color: Colors.white, + ), + fuc: () {}, + tooltip: '下一集', + ), + + /// 时间进度 + BottomControlType.time: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // 播放时间 + Obx(() { + return Text( + Utils.timeFormat(_.positionSeconds.value), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + height: 1.4, + fontFeatures: [FontFeature.tabularFigures()], + ), + semanticsLabel: + '已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}', + ); + }), + // const SizedBox(width: 2), + // const ExcludeSemantics( + // child: Text( + // '/', + // style: textStyle, + // ), + // ), + // const SizedBox(width: 2), + Obx( + () => Text( + Utils.timeFormat(_.durationSeconds.value), + style: const TextStyle( + color: Color(0xFFD0D0D0), + fontSize: 10, + height: 1.4, + fontFeatures: [FontFeature.tabularFigures()], + ), + semanticsLabel: + '共${Utils.durationReadFormat(Utils.timeFormat(_.durationSeconds.value))}', + ), + ), + ], + ), + + /// 空白占位 + BottomControlType.space: const Spacer(), + + /// 选集 + BottomControlType.episode: Obx(() { + bool isSeason = + videoIntroController?.videoDetail.value.ugcSeason != null; + bool isPage = videoIntroController?.videoDetail.value.pages != null && + videoIntroController!.videoDetail.value.pages!.length > 1; + bool isBangumi = bangumiIntroController?.bangumiDetail.value != null; + if (!isSeason && !isPage && !isBangumi) { + return const SizedBox.shrink(); + } + return SizedBox( + width: 42, + height: 30, + child: Container( + width: 42, + height: 30, + alignment: Alignment.center, + child: ComBtn( + icon: const Icon( + Icons.list, + size: 22, + color: Colors.white, + ), + tooltip: '选集', + fuc: () { + int currentCid = widget.controller.cid; + String bvid = widget.controller.bvid; + final List episodes = []; + late Function changeFucCall; + if (isSeason) { + final List sections = videoIntroController! + .videoDetail.value.ugcSeason!.sections!; + for (int i = 0; i < sections.length; i++) { + final List episodesList = + sections[i].episodes!; + episodes.addAll(episodesList); + } + changeFucCall = videoIntroController!.changeSeasonOrbangu; + } else if (isPage) { + final List pages = + videoIntroController!.videoDetail.value.pages!; + episodes.addAll(pages); + changeFucCall = videoIntroController!.changeSeasonOrbangu; + } else if (isBangumi) { + episodes.addAll(bangumiIntroController!.bangumiDetail.value.episodes!); + changeFucCall = bangumiIntroController!.changeSeasonOrbangu; + } + ListSheet( + episodes: episodes, + bvid: bvid, + aid: IdUtils.bv2av(bvid), + currentCid: currentCid, + changeFucCall: changeFucCall, + context: context, + ).buildShowBottomSheet(); + }, + ))); + }), + + /// 画面比例 + BottomControlType.fit: SizedBox( + width: 42, + height: 30, + child: TextButton( + onPressed: () => _.toggleVideoFit(), + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + child: Obx( + () => Text( + _.videoFitDEsc.value, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + ), + + /// 字幕 + BottomControlType.subtitle: Obx( + () => _.vttSubtitles.isEmpty + ? const SizedBox.shrink() + : SizedBox( + width: 42, + height: 30, + child: PopupMenuButton>( + onSelected: (Map value) { + _.setSubtitle(value); + }, + initialValue: _.vttSubtitles[_.vttSubtitlesIndex.value], + color: Colors.black.withOpacity(0.8), + itemBuilder: (BuildContext context) { + return _.vttSubtitles.map((Map subtitle) { + return PopupMenuItem>( + value: subtitle, + child: Text( + "${subtitle['title']}", + style: const TextStyle(color: Colors.white), + ), + ); + }).toList(); + }, + child: Container( + width: 42, + height: 30, + alignment: Alignment.center, + child: const Icon( + Icons.closed_caption_off_outlined, + size: 22, + color: Colors.white, + semanticLabel: '字幕', + ), + ), + ), + ), + ), + + /// 播放速度 + BottomControlType.speed: SizedBox( + width: 42, + height: 30, + child: PopupMenuButton( + onSelected: (double value) { + _.setPlaybackSpeed(value); + }, + initialValue: _.playbackSpeed, + color: Colors.black.withOpacity(0.8), + itemBuilder: (BuildContext context) { + return _.speedsList.map((double speed) { + return PopupMenuItem( + height: 35, + padding: const EdgeInsets.only(left: 30), + value: speed, + child: Text( + "${speed}X", + style: const TextStyle(color: Colors.white, fontSize: 13), + semanticsLabel: "$speed倍速", + ), + ); + }).toList(); + }, + child: Container( + width: 42, + height: 30, + alignment: Alignment.center, + child: Obx(() => Text("${_.playbackSpeed}X", + style: const TextStyle(color: Colors.white, fontSize: 13), + semanticsLabel: "${_.playbackSpeed}倍速")), + ), + ), + ), + + /// 全屏 + BottomControlType.fullscreen: SizedBox( + width: 42, + height: 30, + child: Obx(() => ComBtn( + tooltip: _.isFullScreen.value ? '退出全屏' : '全屏', + icon: Icon( + _.isFullScreen.value ? Icons.fullscreen_exit : Icons.fullscreen, + size: 24, + color: Colors.white, + ), + fuc: () => _.triggerFullScreen!(status: !_.isFullScreen.value), + )), + ), + }; + final List list = []; + var userSpecifyItem = widget.bottomList ?? + [ + BottomControlType.playOrPause, + BottomControlType.time, + // BottomControlType.pre, + // BottomControlType.next, + BottomControlType.space, + BottomControlType.episode, + BottomControlType.fit, + BottomControlType.subtitle, + BottomControlType.speed, + BottomControlType.fullscreen, + ]; + for (var i = 0; i < userSpecifyItem.length; i++) { + list.add(videoProgressWidgets[userSpecifyItem[i]]!); + } + return list; + } + @override Widget build(BuildContext context) { final PlPlayerController _ = widget.controller; @@ -318,7 +594,7 @@ class _PLVideoPlayerState extends State () => Align( child: AnimatedOpacity( curve: Curves.easeInOut, - opacity: _ctr.volumeIndicator.value ? 1.0 : 0.0, + opacity: _volumeIndicator.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( alignment: Alignment.center, @@ -337,9 +613,9 @@ class _PLVideoPlayerState extends State width: 28.0, alignment: Alignment.centerRight, child: Icon( - _ctr.volumeValue.value == 0.0 + _volumeValue.value == 0.0 ? Icons.volume_off - : _ctr.volumeValue.value < 0.5 + : _volumeValue.value < 0.5 ? Icons.volume_down : Icons.volume_up, color: const Color(0xFFFFFFFF), @@ -348,7 +624,7 @@ class _PLVideoPlayerState extends State ), Expanded( child: Text( - '${(_ctr.volumeValue.value * 100.0).round()}%', + '${(_volumeValue.value * 100.0).round()}%', textAlign: TextAlign.center, style: const TextStyle( fontSize: 13.0, @@ -369,7 +645,7 @@ class _PLVideoPlayerState extends State () => Align( child: AnimatedOpacity( curve: Curves.easeInOut, - opacity: _ctr.brightnessIndicator.value ? 1.0 : 0.0, + opacity: _brightnessIndicator.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( alignment: Alignment.center, @@ -388,9 +664,9 @@ class _PLVideoPlayerState extends State width: 28.0, alignment: Alignment.centerRight, child: Icon( - _ctr.brightnessValue.value < 1.0 / 3.0 + _brightnessValue.value < 1.0 / 3.0 ? Icons.brightness_low - : _ctr.brightnessValue.value < 2.0 / 3.0 + : _brightnessValue.value < 2.0 / 3.0 ? Icons.brightness_medium : Icons.brightness_high, color: const Color(0xFFFFFFFF), @@ -400,7 +676,7 @@ class _PLVideoPlayerState extends State const SizedBox(width: 2.0), Expanded( child: Text( - '${(_ctr.brightnessValue.value * 100.0).round()}%', + '${(_brightnessValue.value * 100.0).round()}%', textAlign: TextAlign.center, style: const TextStyle( fontSize: 13.0, @@ -528,7 +804,7 @@ class _PLVideoPlayerState extends State // 左边区域 👈 final double level = renderBox.size.height * 3; final double brightness = - _ctr.brightnessValue.value - delta / level; + _brightnessValue.value - delta / level; final double result = brightness.clamp(0.0, 1.0); setBrightness(result); } else if (tapPosition < sectionWidth * 2) { @@ -540,28 +816,28 @@ class _PLVideoPlayerState extends State await widget.controller.triggerFullScreen(status: status); } - if (dy > _distance && dy > threshold) { + if (dy > _distance.value && dy > threshold) { // 下滑退出全屏/进入全屏 if (_.isFullScreen.value ^ fullScreenGestureReverse) { fullScreenTrigger(fullScreenGestureReverse); } - _distance = 0.0; - } else if (dy < _distance && dy < -threshold) { + _distance.value = 0.0; + } else if (dy < _distance.value && dy < -threshold) { // 上划进入全屏/退出全屏 if (!_.isFullScreen.value ^ fullScreenGestureReverse) { fullScreenTrigger(!fullScreenGestureReverse); } - _distance = 0.0; + _distance.value = 0.0; } - _distance = dy; + _distance.value = dy; } else { // 右边区域 👈 final double level = renderBox.size.height * 0.5; if (lastVolume < 0) { - lastVolume = _ctr.volumeValue.value; + lastVolume = _volumeValue.value; } final double volume = - (lastVolume + _ctr.volumeValue.value - delta / level) / 2; + (lastVolume + _volumeValue.value - delta / level) / 2; final double result = volume.clamp(0.0, 1.0); lastVolume = result; setVolume(result); @@ -596,9 +872,9 @@ class _PLVideoPlayerState extends State position: 'bottom', child: widget.bottomControl ?? BottomControl( - controller: widget.controller, - triggerFullScreen: - widget.controller.triggerFullScreen), + controller: widget.controller, + buildBottomControl: buildBottomControl(), + ), ), ), ], @@ -618,23 +894,23 @@ class _PLVideoPlayerState extends State } if (defaultBtmProgressBehavior == BtmProgresBehavior.alwaysHide.code) { - return Container(); + return const SizedBox(); } if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyShowFullScreen.code && !_.isFullScreen.value) { - return Container(); + return const SizedBox(); } else if (defaultBtmProgressBehavior == BtmProgresBehavior.onlyHideFullScreen.code && _.isFullScreen.value) { - return Container(); + return const SizedBox(); } if (_.videoType.value == 'live') { return Container(); } if (value > max || max <= 0) { - return Container(); + return const SizedBox(); } return Positioned( bottom: -1, @@ -752,18 +1028,17 @@ class _PLVideoPlayerState extends State /// 点击 快进/快退 Obx( () => Visibility( - visible: _ctr.mountSeekBackwardButton.value || - _ctr.mountSeekForwardButton.value, + visible: + _mountSeekBackwardButton.value || _mountSeekForwardButton.value, child: Positioned.fill( child: Row( children: [ Expanded( - child: _ctr.mountSeekBackwardButton.value + child: _mountSeekBackwardButton.value ? TweenAnimationBuilder( tween: Tween( begin: 0.0, - end: - _ctr.hideSeekBackwardButton.value ? 0.0 : 1.0, + end: _hideSeekBackwardButton.value ? 0.0 : 1.0, ), duration: const Duration(milliseconds: 500), builder: (BuildContext context, double value, @@ -773,18 +1048,16 @@ class _PLVideoPlayerState extends State child: child, ), onEnd: () { - if (_ctr.hideSeekBackwardButton.value) { - _ctr.hideSeekBackwardButton.value = false; - _ctr.mountSeekBackwardButton.value = false; + if (_hideSeekBackwardButton.value) { + _hideSeekBackwardButton.value = false; + _mountSeekBackwardButton.value = false; } }, child: BackwardSeekIndicator( - onChanged: (Duration value) { - // _seekBarDeltaValueNotifier.value = -value; - }, + onChanged: (Duration value) => {}, onSubmitted: (Duration value) { - _ctr.hideSeekBackwardButton.value = true; - _ctr.mountSeekBackwardButton.value = false; + _hideSeekBackwardButton.value = true; + _mountSeekBackwardButton.value = false; final Player player = widget.controller.videoPlayerController!; Duration result = player.state.position - value; @@ -797,7 +1070,7 @@ class _PLVideoPlayerState extends State }, ), ) - : nil, + : const SizedBox(), ), const Spacer(), // Expanded( @@ -806,11 +1079,11 @@ class _PLVideoPlayerState extends State // ), // ), Expanded( - child: _ctr.mountSeekForwardButton.value + child: _mountSeekForwardButton.value ? TweenAnimationBuilder( tween: Tween( begin: 0.0, - end: _ctr.hideSeekForwardButton.value ? 0.0 : 1.0, + end: _hideSeekForwardButton.value ? 0.0 : 1.0, ), duration: const Duration(milliseconds: 500), builder: (BuildContext context, double value, @@ -820,18 +1093,16 @@ class _PLVideoPlayerState extends State child: child, ), onEnd: () { - if (_ctr.hideSeekForwardButton.value) { - _ctr.hideSeekForwardButton.value = false; - _ctr.mountSeekForwardButton.value = false; + if (_hideSeekForwardButton.value) { + _hideSeekForwardButton.value = false; + _mountSeekForwardButton.value = false; } }, child: ForwardSeekIndicator( - onChanged: (Duration value) { - // _seekBarDeltaValueNotifier.value = value; - }, + onChanged: (Duration value) => {}, onSubmitted: (Duration value) { - _ctr.hideSeekForwardButton.value = true; - _ctr.mountSeekForwardButton.value = false; + _hideSeekForwardButton.value = true; + _mountSeekForwardButton.value = false; final Player player = widget.controller.videoPlayerController!; Duration result = player.state.position + value; @@ -844,7 +1115,7 @@ class _PLVideoPlayerState extends State }, ), ) - : nil, + : const SizedBox(), ), ], ), @@ -855,31 +1126,3 @@ class _PLVideoPlayerState extends State ); } } - -class PLVideoPlayerController extends GetxController { - RxBool mountSeekBackwardButton = false.obs; - RxBool mountSeekForwardButton = false.obs; - RxBool hideSeekBackwardButton = false.obs; - RxBool hideSeekForwardButton = false.obs; - - RxDouble brightnessValue = 0.0.obs; - RxBool brightnessIndicator = false.obs; - - RxDouble volumeValue = 0.0.obs; - RxBool volumeIndicator = false.obs; - - RxDouble distance = 0.0.obs; - // 初始手指落下位置 - RxDouble initTapPositoin = 0.0.obs; - - RxBool volumeInterceptEventStream = false.obs; - - // 双击快进 展示样式 - void onDoubleTapSeekForward() { - mountSeekForwardButton.value = true; - } - - void onDoubleTapSeekBackward() { - mountSeekBackwardButton.value = true; - } -} diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index b5e7b7ca..eb6f3dc1 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -13,9 +13,12 @@ import '../../../utils/utils.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; - final Function? triggerFullScreen; - const BottomControl({this.controller, this.triggerFullScreen, Key? key}) - : super(key: key); + final List? buildBottomControl; + const BottomControl({ + this.controller, + this.buildBottomControl, + Key? key, + }) : super(key: key); @override Size get preferredSize => const Size(double.infinity, kToolbarHeight); @@ -95,147 +98,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { }, ), Row( - children: [ - controller != null - ? PlayOrPauseButton( - controller: _, - ) - : nil, - // 播放时间 - Obx(() { - return Text( - Utils.timeFormat(_.positionSeconds.value), - style: textStyle, - semanticsLabel: - '已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}', - ); - }), - const SizedBox(width: 2), - const ExcludeSemantics( - child: Text( - '/', - style: textStyle, - ), - ), - const SizedBox(width: 2), - Obx( - () => Text( - Utils.timeFormat(_.durationSeconds.value), - style: textStyle, - semanticsLabel: - '共${Utils.durationReadFormat(Utils.timeFormat(_.durationSeconds.value))}', - ), - ), - const Spacer(), - SizedBox( - width: 42, - height: 30, - child: TextButton( - onPressed: () => _.toggleVideoFit(), - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - child: Obx( - () => Text( - _.videoFitDEsc.value, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - ), - ), - ), - Obx( - () => _.vttSubtitles.isEmpty - ? const SizedBox( - width: 0, - ) - : SizedBox( - width: 42, - height: 30, - child: PopupMenuButton>( - onSelected: (Map value) { - controller!.setSubtitle(value); - }, - initialValue: - _.vttSubtitles[_.vttSubtitlesIndex.value], - color: Colors.black.withOpacity(0.8), - itemBuilder: (BuildContext context) { - return _.vttSubtitles - .map((Map subtitle) { - return PopupMenuItem>( - value: subtitle, - child: Text( - "${subtitle['title']}", - style: const TextStyle(color: Colors.white), - ), - ); - }).toList(); - }, - child: Container( - width: 42, - height: 30, - alignment: Alignment.center, - child: const Icon( - Icons.closed_caption_off_outlined, - size: 22, - color: Colors.white, - semanticLabel: '字幕', - ), - ), - ), - ), - ), - SizedBox( - width: 42, - height: 30, - child: PopupMenuButton( - onSelected: (double value) { - controller!.setPlaybackSpeed(value); - }, - initialValue: _.playbackSpeed, - color: Colors.black.withOpacity(0.8), - itemBuilder: (BuildContext context) { - return _.speedsList.map((double speed) { - return PopupMenuItem( - height: 35, - padding: const EdgeInsets.only(left: 30), - value: speed, - child: Text( - "${speed}X", - style: const TextStyle(color: Colors.white, fontSize: 13), - semanticsLabel: "$speed倍速", - ), - ); - }).toList(); - }, - child: Container( - width: 42, - height: 30, - alignment: Alignment.center, - child: Obx(() => Text("${_.playbackSpeed}X", - style: - const TextStyle(color: Colors.white, fontSize: 13), - semanticsLabel: "${_.playbackSpeed}倍速")), - ), - ), - ), - // 全屏 - SizedBox( - width: 42, - height: 30, - child: Obx(() => ComBtn( - tooltip: _.isFullScreen.value ? '退出全屏' : '全屏', - icon: Icon( - _.isFullScreen.value - ? Icons.fullscreen_exit - : Icons.fullscreen, - size: 24, - color: Colors.white, - ), - fuc: () => - triggerFullScreen!(status: !_.isFullScreen.value), - )), - ), - ], + children: [...buildBottomControl!], ), const SizedBox(height: 12), ], diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index c5128ed7..6a8e6068 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -35,7 +35,7 @@ class Utils { static String numFormat(dynamic number) { if (number == null) { - return '0'; + return '00:00'; } if (number is String) { return number; @@ -126,27 +126,20 @@ class Utils { } static String timeFormat(dynamic time) { - // 1小时内 if (time is String && time.contains(':')) { return time; } - if (time < 3600) { - if (time == 0) { - return time.toString(); - } - final int minute = time ~/ 60; - final double res = time / 60; - if (minute != res) { - return '${minute < 10 ? '0$minute' : minute}:${(time - minute * 60) < 10 ? '0${(time - minute * 60)}' : (time - minute * 60)}'; - } else { - return '$minute:00'; - } - } else { - final int hour = time ~/ 3600; - final String hourStr = hour < 10 ? '0$hour' : hour.toString(); - var a = timeFormat(time - hour * 3600); - return '$hourStr:$a'; + if (time == null || time == 0) { + return '00:00'; } + int hour = time ~/ 3600; + int minute = (time % 3600) ~/ 60; + int second = time % 60; + String paddingStr(int number) { + return number.toString().padLeft(2, '0'); + } + + return '${hour > 0 ? "${paddingStr(hour)}:" : ""}${paddingStr(minute)}:${paddingStr(second)}'; } // 完全相对时间显示