feat: 全屏选择合集

This commit is contained in:
guozhigq
2024-04-06 21:32:29 +08:00
parent d1e83e4d1c
commit c2a4d80c79
10 changed files with 222 additions and 65 deletions

View File

@ -11,6 +11,7 @@ class EpisodeBottomSheet {
final Function changeFucCall; final Function changeFucCall;
final int? cid; final int? cid;
final double? sheetHeight; final double? sheetHeight;
bool isFullScreen = false;
EpisodeBottomSheet({ EpisodeBottomSheet({
required this.episodes, required this.episodes,
@ -20,6 +21,7 @@ class EpisodeBottomSheet {
required this.changeFucCall, required this.changeFucCall,
this.cid, this.cid,
this.sheetHeight, this.sheetHeight,
this.isFullScreen = false,
}); });
Widget buildEpisodeListItem( Widget buildEpisodeListItem(
@ -74,60 +76,69 @@ class EpisodeBottomSheet {
'合集(${episodes.length}', '合集(${episodes.length}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
actions: [ actions: !isFullScreen
IconButton( ? [
icon: const Icon(Icons.close, size: 20), IconButton(
onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close, size: 20),
), onPressed: () => Navigator.pop(context),
const SizedBox(width: 14), ),
], const SizedBox(width: 14),
]
: null,
); );
} }
Widget buildShowContent(BuildContext context) {
final ItemScrollController itemScrollController = ItemScrollController();
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
itemScrollController.jumpTo(index: currentIndex);
});
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
buildTitle(),
Expanded(
child: Material(
child: PageStorage(
bucket: PageStorageBucket(),
child: ScrollablePositionedList.builder(
itemScrollController: itemScrollController,
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,
);
},
),
),
),
),
],
),
);
});
}
/// The [BuildContext] of the widget that calls the bottom sheet. /// The [BuildContext] of the widget that calls the bottom sheet.
PersistentBottomSheetController show(BuildContext context) { PersistentBottomSheetController show(BuildContext context) {
final ItemScrollController itemScrollController = ItemScrollController();
int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid);
final PersistentBottomSheetController btmSheetCtr = showBottomSheet( final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return StatefulBuilder( return buildShowContent(context);
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
itemScrollController.jumpTo(index: currentIndex);
});
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
buildTitle(),
Expanded(
child: Material(
child: ScrollablePositionedList.builder(
itemScrollController: itemScrollController,
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,
);
},
),
),
),
],
),
);
});
}, },
); );
return btmSheetCtr; return btmSheetCtr;

View File

@ -15,6 +15,10 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../common/pages_bottom_sheet.dart';
import '../../../models/common/video_episode_type.dart';
import '../../../utils/drawer.dart';
class BangumiIntroController extends GetxController { class BangumiIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
@ -291,4 +295,29 @@ class BangumiIntroController extends GetxController {
int aid = episodes[nextIndex].aid!; int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid); changeSeasonOrbangu(bvid, cid, aid);
} }
// 播放器底栏 选集 回调
void showEposideHandler() {
late List episodes = bangumiDetail.value.episodes!;
VideoEpidoesType dataType = VideoEpidoesType.bangumiEpisode;
if (episodes.isEmpty) {
return;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
DrawerUtils.showRightDialog(
child: EpisodeBottomSheet(
episodes: episodes,
currentCid: videoDetailCtr.cid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
} }

View File

@ -18,6 +18,9 @@ import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import '../../../../common/pages_bottom_sheet.dart';
import '../../../../models/common/video_episode_type.dart';
import '../../../../utils/drawer.dart';
import '../related/index.dart'; import '../related/index.dart';
import 'widgets/group_panel.dart'; import 'widgets/group_panel.dart';
@ -54,7 +57,7 @@ class VideoIntroController extends GetxController {
bool isPaused = false; bool isPaused = false;
String heroTag = ''; String heroTag = '';
late ModelResult modelResult; late ModelResult modelResult;
late PersistentBottomSheetController? bottomSheetController; PersistentBottomSheetController? bottomSheetController;
@override @override
void onInit() { void onInit() {
@ -562,4 +565,48 @@ class VideoIntroController extends GetxController {
hiddenEpisodeBottomSheet() { hiddenEpisodeBottomSheet() {
bottomSheetController?.close(); bottomSheetController?.close();
} }
// 播放器底栏 选集 回调
void showEposideHandler() {
late List episodes;
VideoEpidoesType dataType = VideoEpidoesType.videoEpisode;
if (videoDetail.value.ugcSeason != null) {
dataType = VideoEpidoesType.videoEpisode;
final List<SectionItem> sections = videoDetail.value.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 == lastPlayCid.value) {
episodes = episodesList;
continue;
}
}
}
}
if (videoDetail.value.pages != null &&
videoDetail.value.pages!.length > 1) {
dataType = VideoEpidoesType.videoPart;
episodes = videoDetail.value.pages!;
}
DrawerUtils.showRightDialog(
child: EpisodeBottomSheet(
episodes: episodes,
currentCid: lastPlayCid.value,
dataType: dataType,
context: Get.context!,
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
if (dataType == VideoEpidoesType.videoEpisode) {
changeSeasonOrbangu(IdUtils.av2bv(item.aid), item.cid, item.aid);
}
if (dataType == VideoEpidoesType.videoPart) {
changeSeasonOrbangu(bvid, item.cid, null);
}
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),
);
}
} }

View File

@ -373,7 +373,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
/// 点赞收藏转发 /// 点赞收藏转发
actionGrid(context, videoIntroController), actionGrid(context, videoIntroController),
// 合集 // 合集 videoPart 简洁
if (widget.videoDetail!.ugcSeason != null) ...[ if (widget.videoDetail!.ugcSeason != null) ...[
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
@ -383,11 +383,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
: widget.videoDetail!.pages!.first.cid, : widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => changeFuc: (bvid, cid, aid) =>
videoIntroController.changeSeasonOrbangu(bvid, cid, aid), videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
),
videoIntroCtr: videoIntroController, videoIntroCtr: videoIntroController,
), ),
) )
], ],
// 合集 videoEpisode
if (widget.videoDetail!.pages != null && if (widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[ widget.videoDetail!.pages!.length > 1) ...[
Obx( Obx(

View File

@ -60,6 +60,7 @@ class _PagesPanelState extends State<PagesPanel> {
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
print('pages changeFucCall');
widget.changeFuc?.call(item.cid); widget.changeFuc?.call(item.cid);
currentIndex.value = i; currentIndex.value = i;
_bottomSheetController?.close(); _bottomSheetController?.close();

View File

@ -30,7 +30,7 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes; late List<EpisodeItem> episodes;
late int cid; late int cid;
late int currentIndex; late RxInt currentIndex = (-1).obs;
final String heroTag = Get.arguments['heroTag']; final String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController; late VideoDetailController _videoDetailController;
final ItemScrollController itemScrollController = ItemScrollController(); final ItemScrollController itemScrollController = ItemScrollController();
@ -57,11 +57,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
} }
/// 取对应 season_id 的 episodes /// 取对应 season_id 的 episodes
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid); currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
_videoDetailController.cid.listen((int p0) { _videoDetailController.cid.listen((int p0) {
cid = p0; cid = p0;
setState(() {}); currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid);
}); });
} }
@ -71,9 +70,8 @@ class _SeasonPanelState extends State<SeasonPanel> {
item.cid, item.cid,
item.aid, item.aid,
); );
currentIndex = i; currentIndex.value = i;
_bottomSheetController?.close(); _bottomSheetController?.close();
setState(() {});
} }
Widget buildEpisodeListItem( Widget buildEpisodeListItem(
@ -148,10 +146,10 @@ class _SeasonPanelState extends State<SeasonPanel> {
height: 12, height: 12,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Obx(() => Text(
'${currentIndex + 1}/${episodes.length}', '${currentIndex.value + 1}/${episodes.length}',
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
), )),
const SizedBox(width: 6), const SizedBox(width: 6),
const Icon( const Icon(
Icons.arrow_forward_ios_outlined, Icons.arrow_forward_ios_outlined,

View File

@ -178,6 +178,9 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (isFullScreen) { if (isFullScreen) {
vdCtr.hiddenReplyReplyPanel(); vdCtr.hiddenReplyReplyPanel();
videoIntroController.hiddenEpisodeBottomSheet(); videoIntroController.hiddenEpisodeBottomSheet();
vdCtr.bottomList.insert(3, BottomControlType.episode);
} else {
vdCtr.bottomList.removeAt(3);
} }
}); });
} }
@ -294,17 +297,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
() { () {
return !vdCtr.autoPlay.value return !vdCtr.autoPlay.value
? const SizedBox() ? const SizedBox()
: PLVideoPlayer( : Obx(
controller: plPlayerController!, () => PLVideoPlayer(
headerControl: vdCtr.headerControl, controller: plPlayerController!,
danmuWidget: Obx( headerControl: vdCtr.headerControl,
() => PlDanmaku( danmuWidget: PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()), key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value, cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!, playerController: plPlayerController!,
), ),
bottomList: vdCtr.bottomList,
showEposideCb: () => vdCtr.videoType == SearchType.video
? videoIntroController.showEposideHandler()
: bangumiIntroController.showEposideHandler(),
), ),
bottomList: vdCtr.bottomList,
); );
}, },
); );

View File

@ -4,6 +4,7 @@ enum BottomControlType {
next, next,
time, time,
space, space,
episode,
fit, fit,
speed, speed,
fullscreen, fullscreen,

View File

@ -37,6 +37,7 @@ class PLVideoPlayer extends StatefulWidget {
this.bottomList, this.bottomList,
this.customWidget, this.customWidget,
this.customWidgets, this.customWidgets,
this.showEposideCb,
super.key, super.key,
}); });
@ -49,6 +50,7 @@ class PLVideoPlayer extends StatefulWidget {
final Widget? customWidget; final Widget? customWidget;
final List<Widget>? customWidgets; final List<Widget>? customWidgets;
final Function? showEposideCb;
@override @override
State<PLVideoPlayer> createState() => _PLVideoPlayerState(); State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@ -267,6 +269,24 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 空白占位 /// 空白占位
BottomControlType.space: const Spacer(), BottomControlType.space: const Spacer(),
/// 选集
BottomControlType.episode: SizedBox(
height: 30,
width: 30,
child: TextButton(
onPressed: () {
widget.showEposideCb?.call();
},
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: const Text(
'选集',
style: TextStyle(color: Colors.white, fontSize: 13),
),
),
),
/// 画面比例 /// 画面比例
BottomControlType.fit: SizedBox( BottomControlType.fit: SizedBox(
height: 30, height: 30,

39
lib/utils/drawer.dart Normal file
View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
class DrawerUtils {
static void showRightDialog({
required Widget child,
double width = 400,
bool useSystem = false,
}) {
SmartDialog.show(
alignment: Alignment.topRight,
animationBuilder: (controller, child, animationParam) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(controller.view),
child: child,
);
},
useSystem: useSystem,
maskColor: Colors.black.withOpacity(0.5),
animationTime: const Duration(milliseconds: 200),
builder: (context) => Container(
width: width,
color: Theme.of(context).scaffoldBackgroundColor,
child: SafeArea(
left: false,
right: false,
bottom: false,
child: MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.zero),
child: child,
),
),
),
);
}
}