diff --git a/README.md b/README.md index ea686032a..b0fee0cba 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 显示视频完整合集 - [x] 三连动画 - [x] 番剧三连 - [x] 带图评论 diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 92fa004a2..174a5b826 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi; @@ -12,6 +14,8 @@ import '../../utils/utils.dart'; class ListSheet { ListSheet({ + this.index, + this.sections, required this.episodes, this.bvid, this.aid, @@ -21,6 +25,8 @@ class ListSheet { this.scaffoldState, }); + final dynamic index; + final dynamic sections; final dynamic episodes; final String? bvid; final int? aid; @@ -32,6 +38,8 @@ class ListSheet { late PersistentBottomSheetController bottomSheetController; Widget get listSheetContent => ListSheetContent( + index: index, + sections: sections, episodes: episodes, bvid: bvid, aid: aid, @@ -54,6 +62,8 @@ class ListSheet { class ListSheetContent extends StatefulWidget { const ListSheetContent({ super.key, + this.index = 0, + this.sections, required this.episodes, this.bvid, this.aid, @@ -62,6 +72,8 @@ class ListSheetContent extends StatefulWidget { required this.onClose, }); + final int index; + final dynamic sections; final dynamic episodes; final String? bvid; final int? aid; @@ -73,24 +85,53 @@ class ListSheetContent extends StatefulWidget { State createState() => _ListSheetContentState(); } -class _ListSheetContentState extends State { - final ItemScrollController itemScrollController = ItemScrollController(); +class _ListSheetContentState extends State + with TickerProviderStateMixin { + late List itemScrollController = []; late final int currentIndex = widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ?? 0; - bool reverse = false; + late List reverse; + + bool get _isList => widget.sections is List && widget.sections.length > 1; + TabController? _ctr; + StreamController? _indexStream; @override void initState() { super.initState(); + if (_isList) { + _indexStream = StreamController(); + _ctr = TabController( + vsync: this, + length: widget.sections.length, + initialIndex: widget.index, + )..addListener(() { + _indexStream?.add(_ctr?.index); + }); + } + itemScrollController = _isList + ? List.generate(widget.sections.length, (_) => ItemScrollController()) + : [ItemScrollController()]; + reverse = + _isList ? List.generate(widget.sections.length, (_) => false) : [false]; WidgetsBinding.instance.addPostFrameCallback((_) { - itemScrollController.jumpTo(index: currentIndex); + itemScrollController[widget.index].jumpTo(index: currentIndex); }); } + @override + void dispose() { + _indexStream?.close(); + _ctr?.removeListener(() {}); + _ctr?.dispose(); + super.dispose(); + } + Widget buildEpisodeListItem( dynamic episode, int index, + int length, bool isCurrentIndex, ) { Color primary = Theme.of(context).colorScheme.primary; @@ -200,7 +241,7 @@ class _ListSheetContentState extends State { ], if (!(episode.runtimeType.toString() == 'EpisodeItem' && (episode.longTitle != null && episode.longTitle != ''))) - Text('${index + 1}/${widget.episodes!.length}'), + Text('${index + 1}/$length'), ], ), ); @@ -219,15 +260,19 @@ class _ListSheetContentState extends State { child: Row( children: [ Text( - '合集(${widget.episodes!.length})', + '合集(${_isList ? List.generate(widget.sections.length, (index) => widget.sections[index].episodes.length).reduce((value, element) => value + element) : widget.episodes!.length})', style: Theme.of(context).textTheme.titleMedium, ), IconButton( tooltip: '跳至顶部', icon: const Icon(Icons.vertical_align_top), onPressed: () { - itemScrollController.scrollTo( - index: !reverse ? 0 : widget.episodes!.length - 1, + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: !reverse[_ctr?.index ?? 0] + ? 0 + : _isList + ? widget.sections[_ctr?.index].episodes.length - 1 + : widget.episodes.length - 1, duration: const Duration(milliseconds: 200), ); }, @@ -236,32 +281,47 @@ class _ListSheetContentState extends State { tooltip: '跳至底部', icon: const Icon(Icons.vertical_align_bottom), onPressed: () { - itemScrollController.scrollTo( - index: !reverse ? widget.episodes!.length - 1 : 0, + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: !reverse[_ctr?.index ?? 0] + ? _isList + ? widget.sections[_ctr?.index].episodes.length - 1 + : widget.episodes.length - 1 + : 0, duration: const Duration(milliseconds: 200), ); }, ), IconButton( + tooltip: '跳至当前', icon: const Icon(Icons.my_location), - onPressed: () { - itemScrollController.scrollTo( + onPressed: () async { + if (_ctr != null && _ctr?.index != widget.index) { + _ctr?.animateTo(widget.index); + await Future.delayed(const Duration(milliseconds: 225)); + } + itemScrollController[_ctr?.index ?? 0].scrollTo( index: currentIndex, duration: const Duration(milliseconds: 200), ); }, ), const Spacer(), - IconButton( - tooltip: '反序', - icon: Icon(!reverse - ? MdiIcons.sortAscending - : MdiIcons.sortDescending), - onPressed: () { - setState(() { - reverse = !reverse; - }); - }, + StreamBuilder( + stream: _indexStream?.stream, + initialData: 0, + builder: (_, snapshot) => IconButton( + tooltip: reverse[snapshot.data] ? '正序' : '反序', + icon: Icon( + !reverse[snapshot.data] + ? MdiIcons.sortAscending + : MdiIcons.sortDescending, + ), + onPressed: () { + setState(() { + reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0]; + }); + }, + ), ), IconButton( tooltip: '关闭', @@ -275,30 +335,55 @@ class _ListSheetContentState extends State { height: 1, color: Theme.of(context).dividerColor.withOpacity(0.1), ), - Expanded( - child: Material( - child: ScrollablePositionedList.separated( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 20), - reverse: reverse, - itemCount: widget.episodes!.length, - itemBuilder: (BuildContext context, int index) { - return buildEpisodeListItem( - widget.episodes![index], - index, - currentIndex == index, - ); - }, - itemScrollController: itemScrollController, - separatorBuilder: (_, index) => Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - ), + if (_isList) + TabBar( + controller: _ctr, + isScrollable: true, + tabs: (widget.sections as List) + .map((item) => Tab(text: item.title)) + .toList(), + dividerHeight: 1, + dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), ), + Expanded( + child: _isList + ? TabBarView( + controller: _ctr, + children: List.generate( + widget.sections.length, + (index) => + _buildBody(index, widget.sections[index].episodes), + ), + ) + : _buildBody(null, widget.episodes), ), ], ), ); } + + Widget _buildBody(i, episodes) => ScrollablePositionedList.separated( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 20, + ), + reverse: reverse[i ?? 0], + itemCount: episodes.length, + itemBuilder: (BuildContext context, int index) { + return buildEpisodeListItem( + episodes[index], + index, + episodes.length, + i != null + ? i == widget.index + ? currentIndex == index + : false + : currentIndex == index, + ); + }, + itemScrollController: itemScrollController[i ?? 0], + separatorBuilder: (_, index) => Divider( + height: 1, + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ); } diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index fa87204af..ea898b5e5 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -118,10 +118,13 @@ class _BangumiPanelState extends State { padding: WidgetStateProperty.all(EdgeInsets.zero), ), onPressed: () => widget.showEpisodes( - widget.pages, - widget.pages[currentIndex].bvid, - widget.pages[currentIndex].aid, - cid), + null, + null, + widget.pages, + widget.pages[currentIndex].bvid, + widget.pages[currentIndex].aid, + cid, + ), child: Text( '全${widget.pages.length}话', style: const TextStyle(fontSize: 13), diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 20d8348e6..7c0bb0c03 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -523,14 +523,16 @@ class _VideoInfoState extends State with TickerProviderStateMixin { if (!loadingStatus && widget.videoDetail?.pages != null && widget.videoDetail!.pages!.length > 1) ...[ - Obx(() => PagesPanel( - heroTag: heroTag, - pages: widget.videoDetail!.pages!, - cid: videoIntroController.lastPlayCid.value, - bvid: videoIntroController.bvid, - changeFuc: videoIntroController.changeSeasonOrbangu, - showEpisodes: widget.showEpisodes, - )) + Obx( + () => PagesPanel( + heroTag: heroTag, + pages: widget.videoDetail!.pages!, + cid: videoIntroController.lastPlayCid.value, + bvid: videoIntroController.bvid, + changeFuc: videoIntroController.changeSeasonOrbangu, + showEpisodes: widget.showEpisodes, + ), + ), ], ], )), diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart index 985fb18fe..2615f9c72 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -96,7 +96,13 @@ class _PagesPanelState extends State { padding: WidgetStateProperty.all(EdgeInsets.zero), ), onPressed: () => widget.showEpisodes( - episodes, widget.bvid, IdUtils.bv2av(widget.bvid), cid), + null, + null, + episodes, + widget.bvid, + IdUtils.bv2av(widget.bvid), + cid, + ), child: Text( '共${widget.pages.length}集', style: const TextStyle(fontSize: 13), diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 63dc55e7b..51cdb6870 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -25,32 +25,20 @@ class SeasonPanel extends StatefulWidget { class _SeasonPanelState extends State { List? episodes; late int cid; + int? _index; int currentIndex = 0; - // final String heroTag = Get.arguments['heroTag']; - late final String heroTag; late VideoDetailController _videoDetailController; - final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); cid = widget.cid!; - heroTag = widget.heroTag; - _videoDetailController = Get.find(tag: heroTag); + _videoDetailController = + Get.find(tag: widget.heroTag); /// 根据 cid 找到对应集,找到对应 episodes /// 有多个episodes时,只显示其中一个 - /// TODO 同时显示多个合集 - final List sections = widget.ugcSeason.sections!; - 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) { - episodes = episodesList; - continue; - } - } - } + _findEpisode(); if (episodes == null) { return; } @@ -62,6 +50,7 @@ class _SeasonPanelState extends State { currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); _videoDetailController.cid.listen((int p0) { cid = p0; + _findEpisode(); currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); if (!mounted) return; setState(() {}); @@ -79,12 +68,6 @@ class _SeasonPanelState extends State { // setState(() {}); // } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { if (episodes == null) { @@ -103,7 +86,14 @@ class _SeasonPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => widget.showEpisodes(episodes, null, null, cid), + onTap: () => widget.showEpisodes( + _index, + widget.ugcSeason.sections, + episodes, + null, + null, + cid, + ), child: Padding( padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), child: Row( @@ -143,4 +133,18 @@ class _SeasonPanelState extends State { ); }); } + + void _findEpisode() { + final List sections = widget.ugcSeason.sections!; + 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; + break; + } + } + } + } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 85d24d3fd..4b051ed4c 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1312,8 +1312,10 @@ class _VideoDetailPageState extends State ); } - showEpisodes(episodes, bvid, aid, cid) { + showEpisodes(index, sections, episodes, bvid, aid, cid) { ListSheet( + index: index, + sections: sections, episodes: episodes, bvid: bvid, aid: aid,