diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 1ee1839f1..b3402cbfa 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -23,7 +23,7 @@ class ListSheetContent extends StatefulWidget { this.aid, required this.currentCid, required this.changeFucCall, - required this.onClose, + this.onClose, }); final dynamic index; @@ -33,7 +33,7 @@ class ListSheetContent extends StatefulWidget { final int? aid; final int currentCid; final Function changeFucCall; - final VoidCallback onClose; + final VoidCallback? onClose; @override State createState() => _ListSheetContentState(); @@ -42,7 +42,7 @@ class ListSheetContent extends StatefulWidget { class _ListSheetContentState extends State with TickerProviderStateMixin { late List itemScrollController = []; - late final int currentIndex = + late int currentIndex = widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ?? 0; late List reverse; @@ -57,11 +57,19 @@ class _ListSheetContentState extends State int? _seasonFav; StreamController? _favStream; + @override + void didUpdateWidget(ListSheetContent oldWidget) { + super.didUpdateWidget(oldWidget); + currentIndex = widget.episodes! + .indexWhere((dynamic e) => e.cid == widget.currentCid) ?? + 0; + } + @override void initState() { super.initState(); if (_isList) { - _indexStream = StreamController(); + _indexStream ??= StreamController(); _ctr = TabController( vsync: this, length: widget.season.sections.length, @@ -81,7 +89,7 @@ class _ListSheetContentState extends State itemScrollController[_index].jumpTo(index: currentIndex); }); if (widget.bvid != null && widget.season != null) { - _favStream = StreamController(); + _favStream ??= StreamController(); () async { dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid); if (result['status']) { @@ -95,7 +103,9 @@ class _ListSheetContentState extends State @override void dispose() { _favStream?.close(); + _favStream = null; _indexStream?.close(); + _indexStream = null; _ctr?.removeListener(() {}); _ctr?.dispose(); super.dispose(); @@ -137,7 +147,7 @@ class _ListSheetContentState extends State } } SmartDialog.showToast('切换到:$title'); - widget.onClose(); + widget.onClose?.call(); widget.changeFucCall( episode is bangumi.EpisodeItem ? episode.epId : null, episode.runtimeType.toString() == "EpisodeItem" @@ -234,7 +244,7 @@ class _ListSheetContentState extends State child: Row( children: [ Text( - '合集(${_isList ? widget.season.epCount : widget.episodes!.length})', + '合集(${_isList ? widget.season.epCount : widget.episodes?.length ?? ''})', style: Theme.of(context).textTheme.titleMedium, ), StreamBuilder( @@ -324,11 +334,12 @@ class _ListSheetContentState extends State }, ), ), - _mediumButton( - tooltip: '关闭', - icon: Icons.close, - onPressed: widget.onClose, - ), + if (widget.onClose != null) + _mediumButton( + tooltip: '关闭', + icon: Icons.close, + onPressed: widget.onClose, + ), ], ), ), diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index c501da269..cbe65fdf2 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -253,6 +253,12 @@ class _ExtraSettingState extends State { setKey: SettingBoxKey.exapndIntroPanelH, defaultVal: false, ), + SetSwitchItem( + title: '横屏分P/合集列表显示在Tab栏', + leading: Icon(Icons.format_list_numbered_rtl_sharp), + setKey: SettingBoxKey.horizontalSeasonPanel, + defaultVal: false, + ), Obx( () => ListTile( enableFeedback: true, diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index f10fc84d8..191f27b5f 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -12,6 +12,7 @@ import 'package:PiliPalaX/http/init.dart'; import 'package:PiliPalaX/http/user.dart'; import 'package:PiliPalaX/models/video/later.dart'; import 'package:PiliPalaX/models/video/play/subtitle.dart'; +import 'package:PiliPalaX/models/video_detail_res.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; import 'package:PiliPalaX/pages/video/detail/related/controller.dart'; import 'package:PiliPalaX/pages/video/detail/reply/controller.dart'; @@ -223,6 +224,11 @@ class VideoDetailController extends GetxController bool get showReply => videoType == SearchType.video ? _showVideoReply : _showBangumiReply; + late final horizontalSeasonPanel = GStorage.horizontalSeasonPanel; + late int seasonCid = 0; + late RxInt seasonIndex = 0.obs; + late RxList episodes = [].obs; + late final bool enableSponsorBlock; PlayerStatus? playerStatus; StreamSubscription? positionSubscription; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 11df7bd99..86b609464 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -691,13 +691,6 @@ class VideoIntroController extends GetxController episodes.indexWhere((e) => e.cid == lastPlayCid.value); int nextIndex = currentIndex + 1; - int cid = episodes[nextIndex].cid!; - while (cid == -1) { - nextIndex++; - SmartDialog.showToast('当前视频暂不支持播放,自动跳过'); - cid = episodes[nextIndex].cid!; - } - // 列表循环 if (nextIndex >= episodes.length) { if (platRepeat == PlayRepeat.listCycle) { @@ -709,6 +702,18 @@ class VideoIntroController extends GetxController return false; } } + + int cid = episodes[nextIndex].cid!; + + while (cid == -1) { + SmartDialog.showToast('当前视频暂不支持播放,自动跳过'); + nextIndex++; + if (nextIndex >= episodes.length) { + return false; + } + cid = episodes[nextIndex].cid!; + } + final String rBvid = isPages ? bvid : episodes[nextIndex].bvid; final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!; changeSeasonOrbangu(null, rBvid, cid, rAid, null); diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 78d045cb7..879c60f48 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -627,7 +627,11 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 点赞收藏转发 布局样式2 if (!isHorizontal) actionGrid(context, videoIntroController), // 合集 - if (!loadingStatus && widget.videoDetail?.ugcSeason != null) ...[ + if (!loadingStatus && + widget.videoDetail?.ugcSeason != null && + (context.orientation != Orientation.landscape || + (context.orientation == Orientation.landscape && + videoDetailCtr.horizontalSeasonPanel.not))) Obx( () => SeasonPanel( heroTag: heroTag, @@ -641,11 +645,13 @@ class _VideoInfoState extends State with TickerProviderStateMixin { showEpisodes: widget.showEpisodes, pages: widget.videoDetail!.pages, ), - ) - ], + ), if (!loadingStatus && widget.videoDetail?.pages != null && - widget.videoDetail!.pages!.length > 1) ...[ + widget.videoDetail!.pages!.length > 1 && + (context.orientation != Orientation.landscape || + (context.orientation == Orientation.landscape && + videoDetailCtr.horizontalSeasonPanel.not))) ...[ Obx( () => PagesPanel( heroTag: heroTag, diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart index c87e4ca3d..2cdfc2e50 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -30,7 +30,7 @@ class PagesPanel extends StatefulWidget { class _PagesPanelState extends State { late int cid; - late int currentIndex; + late int pageIndex; // final String heroTag = Get.arguments['heroTag']; late final String heroTag; late VideoDetailController _videoDetailController; @@ -42,14 +42,13 @@ class _PagesPanelState extends State { cid = widget.cid; heroTag = widget.heroTag; _videoDetailController = Get.find(tag: heroTag); - currentIndex = widget.pages.indexWhere((Part e) => e.cid == cid); + pageIndex = widget.pages.indexWhere((Part e) => e.cid == cid); _videoDetailController.cid.listen((int p0) { cid = p0; - currentIndex = max(0, widget.pages.indexWhere((Part e) => e.cid == cid)); + pageIndex = max(0, widget.pages.indexWhere((Part e) => e.cid == cid)); if (!mounted) return; const double itemWidth = 150; // 每个列表项的宽度 - final double targetOffset = min( - (currentIndex * itemWidth) - (itemWidth / 2), + final double targetOffset = min((pageIndex * itemWidth) - (itemWidth / 2), _scrollController.position.maxScrollExtent); // 滑动至目标位置 _scrollController.animateTo( @@ -78,7 +77,7 @@ class _PagesPanelState extends State { const Text('视频选集 '), Expanded( child: Text( - ' 正在播放:${widget.pages[currentIndex].pagePart}', + ' 正在播放:${widget.pages[pageIndex].pagePart}', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, @@ -119,7 +118,7 @@ class _PagesPanelState extends State { itemCount: widget.pages.length, itemExtent: 150, itemBuilder: (BuildContext context, int i) { - bool isCurrentIndex = currentIndex == i; + bool isCurrentIndex = pageIndex == i; return Container( width: 150, margin: EdgeInsets.only( diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 24bb55fb1..d962317d1 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/models/video_detail_res.dart'; @@ -12,6 +14,7 @@ class SeasonPanel extends StatefulWidget { required this.heroTag, required this.showEpisodes, required this.pages, + this.onTap, }); final UgcSeason ugcSeason; final int? cid; @@ -19,48 +22,55 @@ class SeasonPanel extends StatefulWidget { final String heroTag; final Function showEpisodes; final List? pages; + final bool? onTap; @override State createState() => _SeasonPanelState(); } class _SeasonPanelState extends State { - List? episodes; - late int cid; - int? _index; int currentIndex = 0; late VideoDetailController _videoDetailController; + StreamSubscription? _listener; @override void initState() { super.initState(); - cid = widget.cid!; _videoDetailController = - Get.find(tag: widget.heroTag); + Get.find(tag: widget.heroTag) + ..seasonCid = widget.cid!; /// 根据 cid 找到对应集,找到对应 episodes /// 有多个episodes时,只显示其中一个 _findEpisode(); - if (episodes == null) { + if (_videoDetailController.episodes.isEmpty) { return; } /// 取对应 season_id 的 episodes // episodes = widget.ugcSeason.sections! // .firstWhere((e) => e.seasonId == widget.ugcSeason.id) - // .episodes!; - currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); - _videoDetailController.cid.listen((int p0) { + // .episodes; + currentIndex = _videoDetailController.episodes.indexWhere( + (EpisodeItem e) => e.cid == _videoDetailController.seasonCid); + _listener = _videoDetailController.cid.listen((int p0) { bool isPart = widget.pages?.indexWhere((item) => item.cid == p0) != -1; if (isPart) return; - cid = p0; + _videoDetailController.seasonCid = p0; _findEpisode(); - currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); + currentIndex = _videoDetailController.episodes.indexWhere( + (EpisodeItem e) => e.cid == _videoDetailController.seasonCid); if (!mounted) return; setState(() {}); }); } + @override + void dispose() { + _listener?.cancel(); + super.dispose(); + } + // void changeFucCall(item, int i) async { // await widget.changeFuc!( // IdUtils.av2bv(item.aid), @@ -74,7 +84,7 @@ class _SeasonPanelState extends State { @override Widget build(BuildContext context) { - if (episodes == null) { + if (_videoDetailController.episodes.isEmpty) { return const SizedBox(); } return Builder(builder: (BuildContext context) { @@ -90,14 +100,16 @@ class _SeasonPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => widget.showEpisodes( - _index, - widget.ugcSeason, - episodes, - _videoDetailController.bvid, - null, - cid, - ), + onTap: widget.onTap == false + ? null + : () => widget.showEpisodes( + _videoDetailController.seasonIndex.value, + widget.ugcSeason, + _videoDetailController.episodes, + _videoDetailController.bvid, + null, + _videoDetailController.seasonCid, + ), child: Padding( padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), child: Row( @@ -118,10 +130,10 @@ class _SeasonPanelState extends State { ), const SizedBox(width: 10), Text( - '${currentIndex + 1}/${episodes!.length}', + '${currentIndex + 1}/${_videoDetailController.episodes.length}', style: Theme.of(context).textTheme.labelMedium, semanticsLabel: - '第${currentIndex + 1}集,共${episodes!.length}集', + '第${currentIndex + 1}集,共${_videoDetailController.episodes.length}集', ), const SizedBox(width: 6), const Icon( @@ -143,9 +155,9 @@ class _SeasonPanelState extends State { for (int i = 0; i < sections.length; i++) { final List episodesList = sections[i].episodes!; for (int j = 0; j < episodesList.length; j++) { - if (episodesList[j].cid == cid) { - _index = i; - episodes = episodesList; + if (episodesList[j].cid == _videoDetailController.seasonCid) { + _videoDetailController.seasonIndex.value = i; + _videoDetailController.episodes.value = episodesList; break; } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 97612456f..43cd689a2 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -13,6 +13,8 @@ import 'package:PiliPalaX/pages/bangumi/introduction/widgets/intro_detail.dart' as bangumi; import 'package:PiliPalaX/pages/video/detail/introduction/widgets/intro_detail.dart' as video; +import 'package:PiliPalaX/pages/video/detail/introduction/widgets/page.dart'; +import 'package:PiliPalaX/pages/video/detail/introduction/widgets/season.dart'; import 'package:PiliPalaX/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPalaX/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPalaX/utils/extension.dart'; @@ -90,6 +92,12 @@ class _VideoDetailPageState extends State // StreamSubscription? _bufferedListener; bool get isFullScreen => plPlayerController?.isFullScreen.value ?? false; + bool get _shouldShowSeasonPanel => + (videoIntroController.videoDetail.value.ugcSeason != null || + ((videoIntroController.videoDetail.value.pages?.length ?? 0) > 1)) && + context.orientation == Orientation.landscape && + videoDetailController.horizontalSeasonPanel; + @override void initState() { super.initState(); @@ -578,6 +586,7 @@ class _VideoDetailPageState extends State videoIntro(), if (videoDetailController.showReply) videoReplyPanel, + if (_shouldShowSeasonPanel) seasonPanel, ], ), ), @@ -628,6 +637,7 @@ class _VideoDetailPageState extends State videoIntro(), if (videoDetailController.showReply) videoReplyPanel, + if (_shouldShowSeasonPanel) seasonPanel, ], ), ), @@ -672,7 +682,9 @@ class _VideoDetailPageState extends State children: [ Expanded(child: videoIntro()), if (videoDetailController.showReply) - Expanded(child: videoReplyPanel) + Expanded(child: videoReplyPanel), + if (_shouldShowSeasonPanel) + Expanded(child: seasonPanel), ], ), ) @@ -715,8 +727,16 @@ class _VideoDetailPageState extends State showIntro: false, showReply: videoDetailController.showReply, ), - if (videoDetailController.showReply) - Expanded(child: videoReplyPanel), + Expanded( + child: TabBarView( + controller: videoDetailController.tabCtr, + children: [ + if (videoDetailController.showReply) + videoReplyPanel, + if (_shouldShowSeasonPanel) seasonPanel, + ], + ), + ), ], ), ), @@ -726,10 +746,10 @@ class _VideoDetailPageState extends State // child: TabBarView( // physics: const ClampingScrollPhysics(), // controller: videoDetailController.tabCtr, - // children: [ + // children: [ // CustomScrollView( // key: const PageStorageKey('简介'), - // slivers: [ + // slivers: [ // if (videoDetailController.videoType == // SearchType.video) ...[ // const VideoIntroPanel(), @@ -817,7 +837,7 @@ class _VideoDetailPageState extends State child: TabBarView( physics: const ClampingScrollPhysics(), controller: videoDetailController.tabCtr, - children: [ + children: [ if (videoDetailController.videoType == SearchType.video && videoDetailController.showRelatedVideo) @@ -829,9 +849,10 @@ class _VideoDetailPageState extends State ), if (videoDetailController.showReply) videoReplyPanel, + if (_shouldShowSeasonPanel) seasonPanel, ], ), - ) + ), ], ), ), @@ -1155,7 +1176,9 @@ class _VideoDetailPageState extends State bool showIntro = true, bool showReply = true, }) { - int length = (showIntro ? 1 : 0) + (showReply ? 1 : 0); + int length = (showIntro ? 1 : 0) + + (showReply ? 1 : 0) + + (_shouldShowSeasonPanel ? 1 : 0); if (videoDetailController.tabCtr.length != length) { videoDetailController.tabCtr = TabController(length: length, vsync: this); } @@ -1198,6 +1221,7 @@ class _VideoDetailPageState extends State text: '评论${_videoReplyController.count.value == -1 ? '' : ' ${_videoReplyController.count.value}'}', ), + if (_shouldShowSeasonPanel) Tab(text: '播放列表'), ], ); @@ -1422,6 +1446,67 @@ class _VideoDetailPageState extends State } } + Widget get seasonPanel => Column( + children: [ + if ((videoIntroController.videoDetail.value.pages?.length ?? 0) > 1) + Obx( + () => Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: PagesPanel( + heroTag: heroTag, + pages: videoIntroController.videoDetail.value.pages!, + cid: videoIntroController.lastPlayCid.value, + bvid: videoIntroController.bvid, + changeFuc: videoIntroController.changeSeasonOrbangu, + showEpisodes: showEpisodes, + ), + ), + ), + if (videoIntroController.videoDetail.value.ugcSeason != null) ...[ + if ((videoIntroController.videoDetail.value.pages?.length ?? 0) > 1) + Divider( + height: 1, + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SeasonPanel( + heroTag: heroTag, + onTap: false, + ugcSeason: videoIntroController.videoDetail.value.ugcSeason!, + cid: videoIntroController.lastPlayCid.value != 0 + ? (videoIntroController + .videoDetail.value.pages?.isNotEmpty == + true + ? videoIntroController + .videoDetail.value.pages!.first.cid + : videoIntroController.lastPlayCid.value) + : videoIntroController.videoDetail.value.pages!.first.cid, + changeFuc: videoIntroController.changeSeasonOrbangu, + showEpisodes: showEpisodes, + pages: videoIntroController.videoDetail.value.pages, + ), + ), + Expanded( + child: Obx( + () => ListSheetContent( + index: videoDetailController.seasonIndex.value, + season: videoIntroController.videoDetail.value.ugcSeason!, + episodes: videoDetailController.episodes, + bvid: videoDetailController.bvid, + aid: IdUtils.bv2av(videoDetailController.bvid), + currentCid: videoDetailController.seasonCid, + changeFucCall: videoDetailController.videoType == + SearchType.media_bangumi + ? bangumiIntroController.changeSeasonOrbangu + : videoIntroController.changeSeasonOrbangu, + ), + ), + ), + ], + ], + ); + Widget get videoReplyPanel => Obx( () => VideoReplyPanel( bvid: videoDetailController.bvid, diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 49c377b2e..55a8c0bdd 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -130,6 +130,9 @@ class GStorage { static bool get exapndIntroPanelH => setting.get(SettingBoxKey.exapndIntroPanelH, defaultValue: false); + static bool get horizontalSeasonPanel => + setting.get(SettingBoxKey.horizontalSeasonPanel, defaultValue: false); + static List get dynamicDetailRatio => setting.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]); @@ -328,6 +331,7 @@ class SettingBoxKey { showBangumiReply = 'showBangumiReply', alwaysExapndIntroPanel = 'alwaysExapndIntroPanel', exapndIntroPanelH = 'exapndIntroPanelH', + horizontalSeasonPanel = 'horizontalSeasonPanel', // Sponsor Block enableSponsorBlock = 'enableSponsorBlock',