feat: show all episodes on listsheet

This commit is contained in:
bggRGjQaUbCoE
2024-10-02 12:12:50 +08:00
parent 2bd19ec5f8
commit da39a8fd1e
7 changed files with 182 additions and 79 deletions

View File

@@ -41,6 +41,7 @@
## feat ## feat
- [x] 显示视频完整合集
- [x] 三连动画 - [x] 三连动画
- [x] 番剧三连 - [x] 番剧三连
- [x] 带图评论 - [x] 带图评论

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/common/constants.dart';
import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi; import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi;
@@ -12,6 +14,8 @@ import '../../utils/utils.dart';
class ListSheet { class ListSheet {
ListSheet({ ListSheet({
this.index,
this.sections,
required this.episodes, required this.episodes,
this.bvid, this.bvid,
this.aid, this.aid,
@@ -21,6 +25,8 @@ class ListSheet {
this.scaffoldState, this.scaffoldState,
}); });
final dynamic index;
final dynamic sections;
final dynamic episodes; final dynamic episodes;
final String? bvid; final String? bvid;
final int? aid; final int? aid;
@@ -32,6 +38,8 @@ class ListSheet {
late PersistentBottomSheetController bottomSheetController; late PersistentBottomSheetController bottomSheetController;
Widget get listSheetContent => ListSheetContent( Widget get listSheetContent => ListSheetContent(
index: index,
sections: sections,
episodes: episodes, episodes: episodes,
bvid: bvid, bvid: bvid,
aid: aid, aid: aid,
@@ -54,6 +62,8 @@ class ListSheet {
class ListSheetContent extends StatefulWidget { class ListSheetContent extends StatefulWidget {
const ListSheetContent({ const ListSheetContent({
super.key, super.key,
this.index = 0,
this.sections,
required this.episodes, required this.episodes,
this.bvid, this.bvid,
this.aid, this.aid,
@@ -62,6 +72,8 @@ class ListSheetContent extends StatefulWidget {
required this.onClose, required this.onClose,
}); });
final int index;
final dynamic sections;
final dynamic episodes; final dynamic episodes;
final String? bvid; final String? bvid;
final int? aid; final int? aid;
@@ -73,24 +85,53 @@ class ListSheetContent extends StatefulWidget {
State<ListSheetContent> createState() => _ListSheetContentState(); State<ListSheetContent> createState() => _ListSheetContentState();
} }
class _ListSheetContentState extends State<ListSheetContent> { class _ListSheetContentState extends State<ListSheetContent>
final ItemScrollController itemScrollController = ItemScrollController(); with TickerProviderStateMixin {
late List<ItemScrollController> itemScrollController = [];
late final int currentIndex = late final int currentIndex =
widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ?? widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ??
0; 0;
bool reverse = false; late List<bool> reverse;
bool get _isList => widget.sections is List && widget.sections.length > 1;
TabController? _ctr;
StreamController? _indexStream;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (_isList) {
_indexStream = StreamController<int>();
_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((_) { 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( Widget buildEpisodeListItem(
dynamic episode, dynamic episode,
int index, int index,
int length,
bool isCurrentIndex, bool isCurrentIndex,
) { ) {
Color primary = Theme.of(context).colorScheme.primary; Color primary = Theme.of(context).colorScheme.primary;
@@ -200,7 +241,7 @@ class _ListSheetContentState extends State<ListSheetContent> {
], ],
if (!(episode.runtimeType.toString() == 'EpisodeItem' && if (!(episode.runtimeType.toString() == 'EpisodeItem' &&
(episode.longTitle != null && episode.longTitle != ''))) (episode.longTitle != null && episode.longTitle != '')))
Text('${index + 1}/${widget.episodes!.length}'), Text('${index + 1}/$length'),
], ],
), ),
); );
@@ -219,15 +260,19 @@ class _ListSheetContentState extends State<ListSheetContent> {
child: Row( child: Row(
children: [ children: [
Text( 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, style: Theme.of(context).textTheme.titleMedium,
), ),
IconButton( IconButton(
tooltip: '跳至顶部', tooltip: '跳至顶部',
icon: const Icon(Icons.vertical_align_top), icon: const Icon(Icons.vertical_align_top),
onPressed: () { onPressed: () {
itemScrollController.scrollTo( itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse ? 0 : widget.episodes!.length - 1, index: !reverse[_ctr?.index ?? 0]
? 0
: _isList
? widget.sections[_ctr?.index].episodes.length - 1
: widget.episodes.length - 1,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
}, },
@@ -236,32 +281,47 @@ class _ListSheetContentState extends State<ListSheetContent> {
tooltip: '跳至底部', tooltip: '跳至底部',
icon: const Icon(Icons.vertical_align_bottom), icon: const Icon(Icons.vertical_align_bottom),
onPressed: () { onPressed: () {
itemScrollController.scrollTo( itemScrollController[_ctr?.index ?? 0].scrollTo(
index: !reverse ? widget.episodes!.length - 1 : 0, index: !reverse[_ctr?.index ?? 0]
? _isList
? widget.sections[_ctr?.index].episodes.length - 1
: widget.episodes.length - 1
: 0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
}, },
), ),
IconButton( IconButton(
tooltip: '跳至当前',
icon: const Icon(Icons.my_location), icon: const Icon(Icons.my_location),
onPressed: () { onPressed: () async {
itemScrollController.scrollTo( 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, index: currentIndex,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
); );
}, },
), ),
const Spacer(), const Spacer(),
IconButton( StreamBuilder(
tooltip: '反序', stream: _indexStream?.stream,
icon: Icon(!reverse initialData: 0,
? MdiIcons.sortAscending builder: (_, snapshot) => IconButton(
: MdiIcons.sortDescending), tooltip: reverse[snapshot.data] ? '正序' : '反序',
onPressed: () { icon: Icon(
setState(() { !reverse[snapshot.data]
reverse = !reverse; ? MdiIcons.sortAscending
}); : MdiIcons.sortDescending,
}, ),
onPressed: () {
setState(() {
reverse[_ctr?.index ?? 0] = !reverse[_ctr?.index ?? 0];
});
},
),
), ),
IconButton( IconButton(
tooltip: '关闭', tooltip: '关闭',
@@ -275,30 +335,55 @@ class _ListSheetContentState extends State<ListSheetContent> {
height: 1, height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1), color: Theme.of(context).dividerColor.withOpacity(0.1),
), ),
Expanded( if (_isList)
child: Material( TabBar(
child: ScrollablePositionedList.separated( controller: _ctr,
padding: EdgeInsets.only( isScrollable: true,
bottom: MediaQuery.of(context).padding.bottom + 20), tabs: (widget.sections as List)
reverse: reverse, .map((item) => Tab(text: item.title))
itemCount: widget.episodes!.length, .toList(),
itemBuilder: (BuildContext context, int index) { dividerHeight: 1,
return buildEpisodeListItem( dividerColor: Theme.of(context).dividerColor.withOpacity(0.1),
widget.episodes![index],
index,
currentIndex == index,
);
},
itemScrollController: itemScrollController,
separatorBuilder: (_, index) => Divider(
height: 1,
color: 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),
),
);
} }

View File

@@ -118,10 +118,13 @@ class _BangumiPanelState extends State<BangumiPanel> {
padding: WidgetStateProperty.all(EdgeInsets.zero), padding: WidgetStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => widget.showEpisodes( onPressed: () => widget.showEpisodes(
widget.pages, null,
widget.pages[currentIndex].bvid, null,
widget.pages[currentIndex].aid, widget.pages,
cid), widget.pages[currentIndex].bvid,
widget.pages[currentIndex].aid,
cid,
),
child: Text( child: Text(
'${widget.pages.length}', '${widget.pages.length}',
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),

View File

@@ -523,14 +523,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
if (!loadingStatus && if (!loadingStatus &&
widget.videoDetail?.pages != null && widget.videoDetail?.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[ widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel( Obx(
heroTag: heroTag, () => PagesPanel(
pages: widget.videoDetail!.pages!, heroTag: heroTag,
cid: videoIntroController.lastPlayCid.value, pages: widget.videoDetail!.pages!,
bvid: videoIntroController.bvid, cid: videoIntroController.lastPlayCid.value,
changeFuc: videoIntroController.changeSeasonOrbangu, bvid: videoIntroController.bvid,
showEpisodes: widget.showEpisodes, changeFuc: videoIntroController.changeSeasonOrbangu,
)) showEpisodes: widget.showEpisodes,
),
),
], ],
], ],
)), )),

View File

@@ -96,7 +96,13 @@ class _PagesPanelState extends State<PagesPanel> {
padding: WidgetStateProperty.all(EdgeInsets.zero), padding: WidgetStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => widget.showEpisodes( onPressed: () => widget.showEpisodes(
episodes, widget.bvid, IdUtils.bv2av(widget.bvid), cid), null,
null,
episodes,
widget.bvid,
IdUtils.bv2av(widget.bvid),
cid,
),
child: Text( child: Text(
'${widget.pages.length}', '${widget.pages.length}',
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),

View File

@@ -25,32 +25,20 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
List<EpisodeItem>? episodes; List<EpisodeItem>? episodes;
late int cid; late int cid;
int? _index;
int currentIndex = 0; int currentIndex = 0;
// final String heroTag = Get.arguments['heroTag'];
late final String heroTag;
late VideoDetailController _videoDetailController; late VideoDetailController _videoDetailController;
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!; cid = widget.cid!;
heroTag = widget.heroTag; _videoDetailController =
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag); Get.find<VideoDetailController>(tag: widget.heroTag);
/// 根据 cid 找到对应集,找到对应 episodes /// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个 /// 有多个episodes时只显示其中一个
/// TODO 同时显示多个合集 _findEpisode();
final List<SectionItem> sections = widget.ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == cid) {
episodes = episodesList;
continue;
}
}
}
if (episodes == null) { if (episodes == null) {
return; return;
} }
@@ -62,6 +50,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) { _videoDetailController.cid.listen((int p0) {
cid = p0; cid = p0;
_findEpisode();
currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid); currentIndex = episodes!.indexWhere((EpisodeItem e) => e.cid == cid);
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
@@ -79,12 +68,6 @@ class _SeasonPanelState extends State<SeasonPanel> {
// setState(() {}); // setState(() {});
// } // }
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (episodes == null) { if (episodes == null) {
@@ -103,7 +86,14 @@ class _SeasonPanelState extends State<SeasonPanel> {
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () => widget.showEpisodes(episodes, null, null, cid), onTap: () => widget.showEpisodes(
_index,
widget.ugcSeason.sections,
episodes,
null,
null,
cid,
),
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
child: Row( child: Row(
@@ -143,4 +133,18 @@ class _SeasonPanelState extends State<SeasonPanel> {
); );
}); });
} }
void _findEpisode() {
final List<SectionItem> sections = widget.ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == cid) {
_index = i;
episodes = episodesList;
break;
}
}
}
}
} }

View File

@@ -1312,8 +1312,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
); );
} }
showEpisodes(episodes, bvid, aid, cid) { showEpisodes(index, sections, episodes, bvid, aid, cid) {
ListSheet( ListSheet(
index: index,
sections: sections,
episodes: episodes, episodes: episodes,
bvid: bvid, bvid: bvid,
aid: aid, aid: aid,