From f99740ef2dd0ea4648c36a4a6151b03889c23240 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Tue, 1 Apr 2025 23:21:07 +0800 Subject: [PATCH] refa: list sheet Closes #369 Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/episode_panel.dart | 645 ++++++ lib/common/widgets/keep_alive_wrapper.dart | 27 + lib/common/widgets/list_sheet.dart | 531 ----- lib/models/video_detail_res.dart | 26 +- lib/pages/video/detail/index.dart | 2 +- .../video/detail/introduction/controller.dart | 11 +- lib/pages/video/detail/introduction/view.dart | 3 +- .../detail/introduction/widgets/page.dart | 159 +- lib/pages/video/detail/view.dart | 1966 ----------------- lib/pages/video/detail/view_v.dart | 52 +- lib/plugin/pl_player/view.dart | 17 +- lib/router/app_pages.dart | 1 - 12 files changed, 841 insertions(+), 2599 deletions(-) create mode 100644 lib/common/widgets/episode_panel.dart create mode 100644 lib/common/widgets/keep_alive_wrapper.dart delete mode 100644 lib/common/widgets/list_sheet.dart delete mode 100644 lib/pages/video/detail/view.dart diff --git a/lib/common/widgets/episode_panel.dart b/lib/common/widgets/episode_panel.dart new file mode 100644 index 000000000..e46216d6e --- /dev/null +++ b/lib/common/widgets/episode_panel.dart @@ -0,0 +1,645 @@ +import 'dart:math'; + +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/spring_physics.dart'; +import 'package:PiliPlus/common/widgets/stat/stat.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/models/bangumi/info.dart' as bangumi; +import 'package:PiliPlus/models/video_detail_res.dart' as video; +import 'package:PiliPlus/pages/common/common_slide_page.dart'; +import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; +import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; +import 'package:PiliPlus/utils/id_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +enum EpisodeType { part, season, bangumi } + +extension EpisodeTypeExt on EpisodeType { + String get title => ['分P', '合集', '番剧'][index]; +} + +class EpisodePanel extends CommonSlidePage { + const EpisodePanel({ + super.key, + super.enableSlide, + required this.videoIntroController, + required this.heroTag, + required this.type, + // required this.count, + // required this.name, + required this.aid, + required this.bvid, + required this.cid, + required this.cover, + this.showTitle, + required this.list, + this.seasonId, + this.initialTabIndex = 0, + this.isSupportReverse, + this.isReversed, + this.onReverse, + required this.changeFucCall, + this.onClose, + }); + + final VideoIntroController videoIntroController; + final String heroTag; + final EpisodeType type; + // final int count; + // final String name; + final int? aid; + final String bvid; + final int cid; + final String? cover; + final bool? showTitle; + final List list; + final int? seasonId; + final int initialTabIndex; + final bool? isSupportReverse; + final bool? isReversed; + final Function changeFucCall; + final VoidCallback? onReverse; + final VoidCallback? onClose; + + @override + State createState() => _EpisodePanelState(); +} + +class _EpisodePanelState extends CommonSlidePageState + with SingleTickerProviderStateMixin { + // tab + late final TabController _tabController = TabController( + initialIndex: widget.initialTabIndex, + length: widget.list.length, + vsync: this, + )..addListener(listener); + late final RxInt _currentTabIndex = _tabController.index.obs; + + List get _getCurrEpisodes => widget.type == EpisodeType.season + ? widget.list[_currentTabIndex.value].episodes + : widget.list[_currentTabIndex.value]; + + // item + late RxInt _currentItemIndex; + int get _findCurrentItemIndex => max( + 0, + _getCurrEpisodes.indexWhere((item) => item.cid == widget.cid), + ); + + late final List _isReversed; + late final List _itemScrollController; + + // fav + Rx? _favState; + + late bool _isInit = true; + late final Color primary = Theme.of(context).colorScheme.primary; + final height = 120 / StyleString.aspectRatio + 10; + + void listener() { + _currentTabIndex.value = _tabController.index; + } + + @override + void didUpdateWidget(EpisodePanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.showTitle != false) { + return; + } + + void jumpToCurrent() { + int newItemIndex = _findCurrentItemIndex; + if (_currentItemIndex.value != _findCurrentItemIndex) { + _currentItemIndex.value = newItemIndex; + try { + _itemScrollController[_currentTabIndex.value].jumpTo( + index: newItemIndex, + ); + } catch (_) {} + } + } + + // jump to current + if (_currentTabIndex.value != widget.initialTabIndex) { + _tabController.animateTo( + widget.initialTabIndex, + duration: const Duration(milliseconds: 200), + ); + Future.delayed(const Duration(milliseconds: 300)).then((_) { + jumpToCurrent(); + }); + } else { + jumpToCurrent(); + } + } + + @override + void initState() { + super.initState(); + _itemScrollController = + List.generate(widget.list.length, (_) => ItemScrollController()); + _isReversed = List.generate(widget.list.length, (_) => false); + + if (widget.type == EpisodeType.season && Accounts.main.isLogin) { + _favState = LoadingState.loading().obs; + VideoHttp.videoRelation(bvid: widget.bvid).then((result) { + if (result['status']) { + if (result['data']?['season_fav'] is bool) { + _favState!.value = + LoadingState.success(result['data']['season_fav']); + } + } + }); + } + + _currentItemIndex = _findCurrentItemIndex.obs; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isInit = false; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + _itemScrollController[widget.initialTabIndex] + .jumpTo(index: _currentItemIndex.value); + } catch (_) {} + }); + } + }); + } + + @override + void dispose() { + _tabController.removeListener(listener); + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_isInit) { + return CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + ); + } + + return super.build(context); + } + + @override + Widget get buildPage => Material( + color: widget.showTitle == false + ? Colors.transparent + : Theme.of(context).colorScheme.surface, + child: Column( + children: [ + _buildToolbar, + if (widget.type == EpisodeType.season && widget.list.length > 1) + TabBar( + controller: _tabController, + padding: const EdgeInsets.only(right: 60), + isScrollable: true, + tabs: widget.list.map((item) => Tab(text: item.title)).toList(), + dividerHeight: 1, + dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), + ), + Expanded( + child: widget.type == EpisodeType.season + ? Material( + color: Colors.transparent, + child: tabBarView( + controller: _tabController, + children: List.generate( + widget.list.length, + (index) => _buildBody( + index, + widget.list[index].episodes, + ), + ), + ), + ) + : enableSlide + ? slideList() + : buildList, + ), + ], + ), + ); + + @override + Widget get buildList => Material( + color: Colors.transparent, + child: _buildBody(0, widget.list[0]), + ); + + Widget _buildBody(int index, episodes) { + return KeepAliveWrapper( + builder: (context) => ScrollablePositionedList.builder( + padding: EdgeInsets.only( + top: 5, + bottom: MediaQuery.of(context).padding.bottom + 80, + ), + reverse: _isReversed[index], + itemCount: episodes.length, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + final episode = episodes[index]; + return widget.type == EpisodeType.season && + widget.showTitle != false && + episode.pages.length > 1 + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx( + () => _buildEpisodeItem( + episode: episode, + index: index, + length: episodes.length, + isCurrentIndex: + _currentTabIndex.value == widget.initialTabIndex + ? _currentItemIndex.value == index + : false, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 5), + child: PagesPanel( + list: + widget.initialTabIndex == _currentTabIndex.value && + index == _currentItemIndex.value + ? null + : episode.pages, + cover: episode.arc?.pic, + heroTag: widget.heroTag, + videoIntroController: widget.videoIntroController, + bvid: IdUtils.av2bv(episode.aid), + ), + ), + ], + ) + : Obx( + () => _buildEpisodeItem( + episode: episode, + index: index, + length: episodes.length, + isCurrentIndex: + _currentTabIndex.value == widget.initialTabIndex + ? _currentItemIndex.value == index + : false, + ), + ); + }, + itemScrollController: _itemScrollController[index], + ), + ); + } + + Widget _buildEpisodeItem({ + required dynamic episode, + required int index, + required int length, + required bool isCurrentIndex, + }) { + late String title; + String? cover; + num? duration; + int? pubdate; + int? view; + int? danmaku; + + switch (episode) { + case video.Part(): + cover = episode.firstFrame ?? widget.cover; + title = episode.pagePart!; + duration = episode.duration; + pubdate = episode.ctime; + break; + case video.EpisodeItem(): + title = episode.title!; + cover = episode.arc?.pic; + duration = episode.arc?.duration; + pubdate = episode.arc?.pubdate; + view = episode.arc?.stat?.view; + danmaku = episode.arc?.stat?.danmaku; + break; + case bangumi.EpisodeItem(): + if (episode.longTitle != null && episode.longTitle != "") { + dynamic leading = episode.title ?? index + 1; + title = + "${Utils.isStringNumeric(leading) ? '第$leading话' : leading} ${episode.longTitle!}"; + } else { + title = episode.title!; + } + + cover = episode.cover; + duration = episode.duration == null ? null : episode.duration! ~/ 1000; + pubdate = episode.pubTime; + break; + } + + return Material( + color: Colors.transparent, + child: SizedBox( + height: height, + child: InkWell( + onTap: () { + if (episode.badge != null && episode.badge == "会员") { + dynamic userInfo = GStorage.userInfo.get('userInfoCache'); + int vipStatus = 0; + if (userInfo != null) { + vipStatus = userInfo.vipStatus; + } + if (vipStatus != 1) { + SmartDialog.showToast('需要大会员'); + // return; + } + } + SmartDialog.showToast('切换到:$title'); + widget.onClose?.call(); + if (widget.showTitle == false) { + _currentItemIndex.value = index; + } + widget.changeFucCall( + episode is bangumi.EpisodeItem ? episode.epId : null, + episode.runtimeType.toString() == "EpisodeItem" + ? episode.bvid + : widget.bvid, + episode.cid, + episode.runtimeType.toString() == "EpisodeItem" + ? episode.aid + : widget.aid, + cover, + ); + }, + onLongPress: () { + if (cover?.isNotEmpty == true) { + imageSaveDialog(context: context, title: title, cover: cover); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (cover?.isNotEmpty == true) + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + return Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + src: cover, + width: boxConstraints.maxWidth, + height: boxConstraints.maxHeight, + ), + if (duration != null && duration > 0) + PBadge( + text: Utils.timeFormat(duration), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + ], + ); + }, + ), + ) + else if (isCurrentIndex) + Image.asset( + 'assets/images/live.png', + color: primary, + height: 12, + semanticLabel: "正在播放:", + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + title, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .bodyMedium! + .fontSize, + height: 1.42, + letterSpacing: 0.3, + fontWeight: isCurrentIndex ? FontWeight.bold : null, + color: isCurrentIndex ? primary : null, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (pubdate != null) + Text( + Utils.dateFormat(pubdate), + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + height: 1, + color: Theme.of(context).colorScheme.outline, + overflow: TextOverflow.clip, + ), + ), + if (view != null) ...[ + Row( + children: [ + StatView( + context: context, + theme: 'gray', + value: view, + ), + if (danmaku != null) ...[ + const SizedBox(width: 8), + StatDanMu( + context: context, + theme: 'gray', + value: danmaku, + ), + ], + ], + ) + ], + ], + ), + ), + if (episode.badge != null) ...[ + if (episode.badge == '会员') + Image.asset( + 'assets/images/big-vip.png', + height: 20, + semanticLabel: "大会员", + ) + else + Text(episode.badge), + const SizedBox(width: 10), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildFavBtn(LoadingState loadingState) { + return switch (loadingState) { + Success() => mediumButton( + tooltip: loadingState.response ? '取消订阅' : '订阅', + icon: loadingState.response + ? Icons.notifications_off_outlined + : Icons.notifications_active_outlined, + onPressed: () async { + dynamic result = await VideoHttp.seasonFav( + isFav: loadingState.response, + seasonId: widget.seasonId, + ); + if (result['status']) { + SmartDialog.showToast('${loadingState.response ? '取消' : ''}订阅成功'); + _favState!.value = LoadingState.success(!loadingState.response); + } else { + SmartDialog.showToast(result['msg']); + } + }, + ), + _ => const SizedBox.shrink(), + }; + } + + Widget get _buildReverseBtn => mediumButton( + tooltip: widget.isReversed == true ? '正序播放' : '倒序播放', + icon: widget.isReversed == true + ? MdiIcons.sortDescending + : MdiIcons.sortAscending, + onPressed: () { + widget.onReverse?.call(); + }, + ); + + Widget get _buildToolbar => Container( + height: 45, + padding: EdgeInsets.symmetric( + horizontal: widget.showTitle != false ? 14 : 6), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.1), + ), + ), + ), + child: Row( + children: [ + if (widget.showTitle != false) + Text( + widget.type.title, + style: Theme.of(context).textTheme.titleMedium, + ), + if (_favState != null) Obx(() => _buildFavBtn(_favState!.value)), + mediumButton( + tooltip: '跳至顶部', + icon: Icons.vertical_align_top, + onPressed: () { + try { + _itemScrollController[_currentTabIndex.value].scrollTo( + index: !_isReversed[_currentTabIndex.value] + ? 0 + : _getCurrEpisodes.length - 1, + duration: const Duration(milliseconds: 200), + ); + } catch (e) { + debugPrint('to top: $e'); + } + }, + ), + mediumButton( + tooltip: '跳至底部', + icon: Icons.vertical_align_bottom, + onPressed: () { + try { + _itemScrollController[_currentTabIndex.value].scrollTo( + index: !_isReversed[_currentTabIndex.value] + ? _getCurrEpisodes.length - 1 + : 0, + duration: const Duration(milliseconds: 200), + ); + } catch (e) { + debugPrint('to bottom: $e'); + } + }, + ), + mediumButton( + tooltip: '跳至当前', + icon: Icons.my_location, + onPressed: () async { + try { + if (_currentTabIndex.value != widget.initialTabIndex) { + _tabController.animateTo(widget.initialTabIndex); + await Future.delayed(const Duration(milliseconds: 225)); + } + _itemScrollController[_currentTabIndex.value].scrollTo( + index: _currentItemIndex.value, + duration: const Duration(milliseconds: 200), + ); + } catch (_) {} + }, + ), + if (widget.isSupportReverse == true) + Obx( + () { + return _currentTabIndex.value == widget.initialTabIndex + ? _buildReverseBtn + : const SizedBox.shrink(); + }, + ), + const Spacer(), + Obx( + () => mediumButton( + tooltip: _isReversed[_currentTabIndex.value] ? '顺序' : '倒序', + icon: !_isReversed[_currentTabIndex.value] + ? MdiIcons.sortNumericAscending + : MdiIcons.sortNumericDescending, + onPressed: () { + setState(() { + _isReversed[_currentTabIndex.value] = + !_isReversed[_currentTabIndex.value]; + }); + }, + ), + ), + if (widget.onClose != null) + mediumButton( + tooltip: '关闭', + icon: Icons.close, + onPressed: widget.onClose, + ), + ], + ), + ); +} diff --git a/lib/common/widgets/keep_alive_wrapper.dart b/lib/common/widgets/keep_alive_wrapper.dart new file mode 100644 index 000000000..c10508423 --- /dev/null +++ b/lib/common/widgets/keep_alive_wrapper.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class KeepAliveWrapper extends StatefulWidget { + const KeepAliveWrapper({ + super.key, + required this.builder, + this.wantKeepAlive = true, + }); + + final WidgetBuilder builder; + final bool wantKeepAlive; + + @override + State createState() => _KeepAliveWrapperState(); +} + +class _KeepAliveWrapperState extends State + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.builder(context); + } + + @override + bool get wantKeepAlive => widget.wantKeepAlive; +} diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart deleted file mode 100644 index 838c2b29b..000000000 --- a/lib/common/widgets/list_sheet.dart +++ /dev/null @@ -1,531 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/icon_button.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/http/video.dart'; -import 'package:PiliPlus/models/bangumi/info.dart' as bangumi; -import 'package:PiliPlus/models/video_detail_res.dart' as video; -import 'package:PiliPlus/pages/common/common_slide_page.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -import '../../utils/storage.dart'; -import '../../utils/utils.dart'; -import 'package:PiliPlus/common/widgets/spring_physics.dart'; - -class ListSheetContent extends CommonSlidePage { - const ListSheetContent({ - super.key, - this.index, // tab index - this.season, - this.episodes, - this.bvid, - this.aid, - required this.currentCid, - required this.changeFucCall, - this.onClose, - this.onReverse, - this.showTitle, - this.isSupportReverse, - this.isReversed, - super.enableSlide, - }); - - final dynamic index; - final dynamic season; - final dynamic episodes; - final String? bvid; - final int? aid; - final int currentCid; - final Function changeFucCall; - final VoidCallback? onClose; - final VoidCallback? onReverse; - final bool? showTitle; - final bool? isSupportReverse; - final bool? isReversed; - - @override - State createState() => _ListSheetContentState(); -} - -class _ListSheetContentState extends CommonSlidePageState - with TickerProviderStateMixin { - late List itemScrollController = []; - late int currentIndex = _currentIndex; - late List reverse; - - int get _index => widget.index ?? 0; - late final bool _isList = widget.season != null && - widget.season?.sections is List && - widget.season.sections.length > 1; - dynamic get episodes => - widget.episodes ?? widget.season?.sections[_index].episodes; - TabController? _ctr; - StreamController? _indexStream; - int? _seasonFav; - StreamController? _favStream; - - @override - void didUpdateWidget(ListSheetContent oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.showTitle != false) { - return; - } - - int currentIndex = _currentIndex; - - void jumpToCurrent() { - if (this.currentIndex != currentIndex) { - this.currentIndex = currentIndex; - try { - itemScrollController[_index].jumpTo( - index: currentIndex, - ); - } catch (_) {} - } - } - - // jump to current - if (_ctr != null && widget.index != _ctr?.index) { - _ctr?.animateTo(_index, duration: const Duration(milliseconds: 200)); - Future.delayed(const Duration(milliseconds: 300)).then((_) { - jumpToCurrent(); - }); - } else { - jumpToCurrent(); - } - } - - int get _currentIndex => max( - 0, - _isList - ? widget.season.sections[_index].episodes - .indexWhere((e) => e.cid == widget.currentCid) - : episodes.indexWhere((e) => e.cid == widget.currentCid)); - - void listener() { - _indexStream?.add(_ctr?.index); - } - - late bool _isInit = true; - - @override - void initState() { - super.initState(); - if (_isList) { - _indexStream ??= StreamController.broadcast(); - _ctr = TabController( - vsync: this, - length: widget.season.sections.length, - initialIndex: _index, - )..addListener(listener); - } - itemScrollController = _isList - ? List.generate( - widget.season.sections.length, (_) => ItemScrollController()) - : [ItemScrollController()]; - reverse = _isList - ? List.generate(widget.season.sections.length, (_) => false) - : [false]; - if (Accounts.main.isLogin && widget.bvid != null && widget.season != null) { - _favStream ??= StreamController(); - () async { - dynamic result = await VideoHttp.videoRelation(bvid: widget.bvid); - if (result['status']) { - _seasonFav = result['data']['season_fav'] ? 1 : 0; - _favStream?.add(_seasonFav); - } - }(); - } - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _isInit = false; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - try { - itemScrollController[_index].jumpTo(index: currentIndex); - } catch (_) {} - }); - } - }); - } - - @override - void dispose() { - _favStream?.close(); - _favStream = null; - _indexStream?.close(); - _indexStream = null; - _ctr?.removeListener(listener); - _ctr?.dispose(); - super.dispose(); - } - - Widget buildEpisodeListItem( - dynamic episode, - int index, - int length, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - late String title; - if (episode.runtimeType.toString() == "EpisodeItem") { - if (episode.longTitle != null && episode.longTitle != "") { - dynamic leading = episode.title ?? index + 1; - title = - "${Utils.isStringNumeric(leading) ? '第$leading话' : leading} ${episode.longTitle!}"; - } else { - title = episode.title!; - } - } else if (episode.runtimeType.toString() == "PageItem") { - title = episode.pagePart!; - } else if (episode.runtimeType.toString() == "Part") { - title = episode.pagePart!; - // debugPrint("未知类型:${episode.runtimeType}"); - } - return ListTile( - onTap: () { - if (episode.badge != null && episode.badge == "会员") { - dynamic userInfo = GStorage.userInfo.get('userInfoCache'); - int vipStatus = 0; - if (userInfo != null) { - vipStatus = userInfo.vipStatus; - } - if (vipStatus != 1) { - SmartDialog.showToast('需要大会员'); - // return; - } - } - SmartDialog.showToast('切换到:$title'); - widget.onClose?.call(); - currentIndex = index; - widget.changeFucCall( - episode is bangumi.EpisodeItem ? episode.epId : null, - episode.runtimeType.toString() == "EpisodeItem" - ? episode.bvid - : widget.bvid, - episode.cid, - episode.runtimeType.toString() == "EpisodeItem" - ? episode.aid - : widget.aid, - episode is video.EpisodeItem - ? episode.arc?.pic - : episode is bangumi.EpisodeItem - ? episode.cover - : null, - ); - }, - dense: false, - leading: (episode is video.EpisodeItem && episode.arc?.pic != null) || - (episode is video.Part && episode.firstFrame != null) || - (episode is bangumi.EpisodeItem && episode.cover != null) - ? Container( - margin: const EdgeInsets.symmetric(vertical: 6), - decoration: isCurrentIndex - ? BoxDecoration( - borderRadius: BorderRadius.circular(6), - border: Border.all( - width: 1.8, - strokeAlign: BorderSide.strokeAlignOutside, - color: Theme.of(context).colorScheme.primary, - ), - ) - : null, - child: LayoutBuilder( - builder: (context, constraints) => NetworkImgLayer( - radius: 6, - src: episode is video.EpisodeItem - ? episode.arc?.pic - : episode is bangumi.EpisodeItem - ? episode.cover - : episode.firstFrame, - width: constraints.maxHeight * StyleString.aspectRatio, - height: constraints.maxHeight, - ), - ), - ) - : isCurrentIndex - ? Image.asset( - 'assets/images/live.png', - color: primary, - height: 12, - semanticLabel: "正在播放:", - ) - : null, - title: Text( - title, - style: TextStyle( - fontSize: 14, - fontWeight: isCurrentIndex ? FontWeight.bold : null, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (episode.badge != null) ...[ - if (episode.badge == '会员') - Image.asset( - 'assets/images/big-vip.png', - height: 20, - semanticLabel: "大会员", - ) - else - Text(episode.badge), - const SizedBox(width: 10), - ], - if (episode is! bangumi.EpisodeItem) Text('${index + 1}/$length'), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - if (_isInit) { - return CustomScrollView( - physics: const NeverScrollableScrollPhysics(), - ); - } - - return enableSlide - ? Padding( - padding: EdgeInsets.only(top: padding), - child: buildPage, - ) - : buildPage; - } - - @override - Widget get buildPage => Material( - color: widget.showTitle == false - ? Colors.transparent - : Theme.of(context).colorScheme.surface, - child: Column( - children: [ - Container( - height: 45, - padding: EdgeInsets.symmetric( - horizontal: widget.showTitle != false ? 14 : 6), - child: Row( - children: [ - if (widget.showTitle != false) - Text( - '合集(${_isList ? widget.season.epCount : episodes?.length ?? ''})', - style: Theme.of(context).textTheme.titleMedium, - ), - StreamBuilder( - stream: _favStream?.stream, - builder: (context, snapshot) => snapshot.hasData - ? mediumButton( - tooltip: _seasonFav == 1 ? '取消订阅' : '订阅', - icon: _seasonFav == 1 - ? Icons.notifications_off_outlined - : Icons.notifications_active_outlined, - onPressed: () async { - dynamic result = await VideoHttp.seasonFav( - isFav: _seasonFav == 1, - seasonId: widget.season.id, - ); - if (result['status']) { - SmartDialog.showToast( - '${_seasonFav == 1 ? '取消' : ''}订阅成功'); - _seasonFav = _seasonFav == 1 ? 0 : 1; - _favStream?.add(_seasonFav); - } else { - SmartDialog.showToast(result['msg']); - } - }, - ) - : const SizedBox.shrink(), - ), - mediumButton( - tooltip: '跳至顶部', - icon: Icons.vertical_align_top, - onPressed: () { - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: !reverse[_ctr?.index ?? 0] - ? 0 - : _isList - ? widget.season.sections[_ctr?.index].episodes - .length - - 1 - : episodes.length - 1, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - mediumButton( - tooltip: '跳至底部', - icon: Icons.vertical_align_bottom, - onPressed: () { - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: !reverse[_ctr?.index ?? 0] - ? _isList - ? widget.season.sections[_ctr?.index].episodes - .length - - 1 - : episodes.length - 1 - : 0, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - mediumButton( - tooltip: '跳至当前', - icon: Icons.my_location, - onPressed: () async { - if (_ctr != null && _ctr?.index != (_index)) { - _ctr?.animateTo(_index); - await Future.delayed(const Duration(milliseconds: 225)); - } - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: currentIndex, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - }, - ), - if (widget.isSupportReverse == true) - if (!_isList) - _reverseButton - else - StreamBuilder( - stream: _indexStream?.stream, - initialData: _index, - builder: (context, snapshot) { - return snapshot.data == _index - ? _reverseButton - : const SizedBox.shrink(); - }, - ), - const Spacer(), - StreamBuilder( - stream: _indexStream?.stream, - initialData: _index, - builder: (context, snapshot) => mediumButton( - tooltip: reverse[snapshot.data] ? '顺序' : '倒序', - icon: !reverse[snapshot.data] - ? MdiIcons.sortNumericAscending - : MdiIcons.sortNumericDescending, - onPressed: () { - setState(() { - reverse[_ctr?.index ?? 0] = - !reverse[_ctr?.index ?? 0]; - }); - }, - ), - ), - if (widget.onClose != null) - mediumButton( - tooltip: '关闭', - icon: Icons.close, - onPressed: widget.onClose, - ), - ], - ), - ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - if (_isList) - TabBar( - controller: _ctr, - padding: const EdgeInsets.only(right: 60), - isScrollable: true, - tabs: (widget.season.sections as List) - .map((item) => Tab(text: item.title)) - .toList(), - dividerHeight: 1, - dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), - ), - Expanded( - child: _isList - ? Material( - color: Colors.transparent, - child: tabBarView( - controller: _ctr, - children: List.generate( - widget.season.sections.length, - (index) => _buildBody( - index, widget.season.sections[index].episodes), - ), - ), - ) - : enableSlide - ? slideList() - : buildList, - ), - ], - ), - ); - - @override - Widget get buildList => Material( - color: Colors.transparent, - child: _buildBody(null, episodes), - ); - - Widget get _reverseButton => mediumButton( - tooltip: widget.isReversed == true ? '正序播放' : '倒序播放', - icon: widget.isReversed == true - ? MdiIcons.sortDescending - : MdiIcons.sortAscending, - onPressed: () async { - if (widget.showTitle == false) { - // jump to current - if (_ctr != null && _ctr?.index != (_index)) { - _ctr?.animateTo(_index); - await Future.delayed(const Duration(milliseconds: 225)); - } - try { - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: currentIndex, - duration: const Duration(milliseconds: 200), - ); - } catch (_) {} - } - - widget.onReverse?.call(); - }, - ); - - Widget _buildBody(i, episodes) => ScrollablePositionedList.separated( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - reverse: reverse[i ?? 0], - itemCount: episodes.length, - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return buildEpisodeListItem( - episodes[index], - index, - episodes.length, - i != null - ? i == (_index) - ? currentIndex == index - : false - : currentIndex == index, - ); - }, - itemScrollController: itemScrollController[i ?? 0], - separatorBuilder: (context, index) => Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), - ); -} diff --git a/lib/models/video_detail_res.dart b/lib/models/video_detail_res.dart index 37c84bb8e..fcb968d55 100644 --- a/lib/models/video_detail_res.dart +++ b/lib/models/video_detail_res.dart @@ -427,6 +427,7 @@ class Part { Dimension? dimension; String? firstFrame; String? badge; + int? ctime; Part({ this.cid, @@ -439,6 +440,7 @@ class Part { this.dimension, this.firstFrame, this.badge, + this.ctime, }); fromRawJson(String str) => Part.fromJson(json.decode(str)); @@ -458,6 +460,7 @@ class Part { : Dimension.fromJson(json["dimension"]); firstFrame = json["first_frame"]; badge = json["badge"]; + ctime = json["ctime"]; } Map toJson() { @@ -479,7 +482,7 @@ class Part { class Stat { int? aid; int? view; - int? danmu; + int? danmaku; int? reply; int? favorite; int? coin; @@ -493,7 +496,7 @@ class Stat { Stat({ this.aid, this.view, - this.danmu, + this.danmaku, this.reply, this.favorite, this.coin, @@ -512,7 +515,7 @@ class Stat { Stat.fromJson(Map json) { aid = json["aid"]; view = json["view"]; - danmu = json["danmaku"]; + danmaku = json["danmaku"]; reply = json["reply"]; favorite = json["favorite"]; coin = json["coin"]; @@ -529,7 +532,7 @@ class Stat { data["aid"] = aid; data["view"] = view; - data["danmaku"] = danmu; + data["danmaku"] = danmaku; data["reply"] = reply; data["favorite"] = favorite; data["coin"] = coin; @@ -640,12 +643,23 @@ class UgcSeason { class Arc { Arc({ this.pic, + this.duration, + this.pubdate, + this.stat, }); String? pic; + num? duration; + int? pubdate; + Stat? stat; Arc.fromJson(Map json) { pic = json['pic'] as String?; + duration = json['duration']; + pubdate = json['pubdate']; + stat = json['stat'] == null + ? null + : Stat.fromJson(json['stat'] as Map); } } @@ -691,6 +705,7 @@ class EpisodeItem { this.bvid, this.badge, this.arc, + this.pages, }); int? seasonId; int? sectionId; @@ -704,6 +719,7 @@ class EpisodeItem { String? bvid; String? badge; Arc? arc; + List? pages; EpisodeItem.fromJson(Map json) { seasonId = json['season_id']; @@ -720,5 +736,7 @@ class EpisodeItem { arc = json['arc'] == null ? null : Arc.fromJson(json['arc'] as Map); + pages = + (json['pages'] as List?)?.map((json) => Part.fromJson(json)).toList(); } } diff --git a/lib/pages/video/detail/index.dart b/lib/pages/video/detail/index.dart index 22e325ef6..c22ccf691 100644 --- a/lib/pages/video/detail/index.dart +++ b/lib/pages/video/detail/index.dart @@ -1,4 +1,4 @@ library video_detail; export './controller.dart'; -export './view.dart'; +export './view_v.dart'; diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 2c97cce6d..93b66e079 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -778,7 +778,11 @@ class VideoIntroController extends GetxController final int currentIndex = episodes.indexWhere((e) => e.cid == - (skipPages ? videoDetail.value.pages!.first.cid : lastPlayCid.value)); + (skipPages + ? videoDetail.value.isPageReversed == true + ? videoDetail.value.pages!.last.cid + : videoDetail.value.pages!.first.cid + : lastPlayCid.value)); int prevIndex = currentIndex - 1; final PlayRepeat playRepeat = videoDetailCtr.plPlayerController.playRepeat; @@ -838,8 +842,11 @@ class VideoIntroController extends GetxController final int currentIndex = episodes.indexWhere((e) => e.cid == (skipPages - ? videoDetail.value.pages!.first.cid + ? videoDetail.value.isPageReversed == true + ? videoDetail.value.pages!.last.cid + : videoDetail.value.pages!.first.cid : videoDetailCtr.cid.value)); + int nextIndex = currentIndex + 1; if (isPages.not && diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 65b02831d..112a8653f 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -627,7 +627,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { context: context, theme: 'gray', value: Utils.numFormat(!widget.loadingStatus - ? videoDetail.stat?.danmu ?? '-' + ? videoDetail.stat?.danmaku ?? '-' : videoItem['stat']?.danmu ?? '-'), size: 'medium', textColor: t.colorScheme.outline, @@ -858,7 +858,6 @@ class _VideoInfoState extends State with TickerProviderStateMixin { heroTag: widget.heroTag, videoIntroController: videoIntroController, 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 787dc8db1..b5accaccc 100644 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ b/lib/pages/video/detail/introduction/widgets/page.dart @@ -12,16 +12,20 @@ import '../../../../../utils/id_utils.dart'; class PagesPanel extends StatefulWidget { const PagesPanel({ super.key, + this.list, + this.cover, required this.bvid, - required this.changeFuc, required this.heroTag, - required this.showEpisodes, + this.showEpisodes, required this.videoIntroController, }); + + final List? list; + final String? cover; + final String bvid; - final Function changeFuc; final String heroTag; - final Function showEpisodes; + final Function? showEpisodes; final VideoIntroController videoIntroController; @override @@ -30,36 +34,46 @@ class PagesPanel extends StatefulWidget { class _PagesPanelState extends State { late int cid; - late int pageIndex; + int pageIndex = -1; late VideoDetailController _videoDetailController; final ScrollController _scrollController = ScrollController(); StreamSubscription? _listener; - List get pages => widget.videoIntroController.videoDetail.value.pages!; + List get pages => + widget.list ?? widget.videoIntroController.videoDetail.value.pages!; @override void initState() { super.initState(); - cid = widget.videoIntroController.lastPlayCid.value; - _videoDetailController = - Get.find(tag: widget.heroTag); - pageIndex = pages.indexWhere((Part e) => e.cid == cid); - _listener = _videoDetailController.cid.listen((int cid) { - this.cid = cid; - pageIndex = max(0, pages.indexWhere((Part e) => e.cid == cid)); - if (!mounted) return; - setState(() {}); - const double itemWidth = 150; // 每个列表项的宽度 - final double targetOffset = (pageIndex * itemWidth - itemWidth / 2).clamp( - _scrollController.position.minScrollExtent, - _scrollController.position.maxScrollExtent); - // 滑动至目标位置 - _scrollController.animateTo( - targetOffset, - duration: const Duration(milliseconds: 300), // 滑动动画持续时间 - curve: Curves.easeInOut, // 滑动动画曲线 - ); - }); + if (widget.list == null) { + cid = widget.videoIntroController.lastPlayCid.value; + _videoDetailController = + Get.find(tag: widget.heroTag); + pageIndex = pages.indexWhere((Part e) => e.cid == cid); + _listener = _videoDetailController.cid.listen((int cid) { + this.cid = cid; + pageIndex = max(0, pages.indexWhere((Part e) => e.cid == cid)); + if (!mounted) return; + setState(() {}); + jumpToCurr(); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + jumpToCurr(); + }); + } + } + + void jumpToCurr() { + const double itemWidth = 150; // 每个列表项的宽度 + final double targetOffset = (pageIndex * itemWidth - itemWidth / 2).clamp( + _scrollController.position.minScrollExtent, + _scrollController.position.maxScrollExtent); + // 滑动至目标位置 + _scrollController.animateTo( + targetOffset, + duration: const Duration(milliseconds: 300), // 滑动动画持续时间 + curve: Curves.easeInOut, // 滑动动画曲线 + ); } @override @@ -73,46 +87,47 @@ class _PagesPanelState extends State { Widget build(BuildContext context) { return Column( children: [ - Padding( - padding: const EdgeInsets.only(top: 8, bottom: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('视频选集 '), - Expanded( - child: Text( - ' 正在播放:${pages[pageIndex].pagePart}', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - const SizedBox(width: 10), - SizedBox( - height: 34, - child: TextButton( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => widget.showEpisodes( - null, - null, - pages, - widget.bvid, - IdUtils.bv2av(widget.bvid), - cid, - ), + if (widget.showEpisodes != null) + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('视频选集 '), + Expanded( child: Text( - '共${pages.length}集', - style: const TextStyle(fontSize: 13), + ' 正在播放:${pages[pageIndex].pagePart}', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, + ), ), ), - ), - ], + const SizedBox(width: 10), + SizedBox( + height: 34, + child: TextButton( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => widget.showEpisodes!( + null, + null, + pages, + widget.bvid, + IdUtils.bv2av(widget.bvid), + cid, + ), + child: Text( + '共${pages.length}集', + style: const TextStyle(fontSize: 13), + ), + ), + ), + ], + ), ), - ), SizedBox( height: 35, child: ListView.builder( @@ -132,14 +147,17 @@ class _PagesPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => { - widget.changeFuc( + onTap: () { + if (widget.showEpisodes == null) { + Get.back(); + } + widget.videoIntroController.changeSeasonOrbangu( null, widget.bvid, pages[i].cid, IdUtils.bv2av(widget.bvid), - null, - ) + widget.cover, + ); }, child: Padding( padding: const EdgeInsets.symmetric( @@ -160,10 +178,11 @@ class _PagesPanelState extends State { pages[i].pagePart!, maxLines: 1, style: TextStyle( - fontSize: 13, - color: isCurrentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), + fontSize: 13, + color: isCurrentIndex + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), overflow: TextOverflow.ellipsis, )) ], diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart deleted file mode 100644 index 87b9d5340..000000000 --- a/lib/pages/video/detail/view.dart +++ /dev/null @@ -1,1966 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; - -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/list_sheet.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/main.dart'; -import 'package:PiliPlus/models/common/reply_type.dart'; -import 'package:PiliPlus/pages/bangumi/introduction/widgets/intro_detail.dart' - as bangumi; -import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; -import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; -import 'package:PiliPlus/pages/video/detail/member/controller.dart'; -import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; -import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; -import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart'; -import 'package:PiliPlus/pages/video/detail/widgets/ai_detail.dart'; -import 'package:PiliPlus/utils/download.dart'; -import 'package:PiliPlus/utils/extension.dart'; -import 'package:PiliPlus/utils/id_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; -import 'package:auto_orientation/auto_orientation.dart'; -import 'package:easy_debounce/easy_throttle.dart'; -import 'package:floating/floating.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import 'package:hive/hive.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/models/common/search_type.dart'; -import 'package:PiliPlus/pages/bangumi/introduction/index.dart'; -import 'package:PiliPlus/pages/danmaku/view.dart'; -import 'package:PiliPlus/pages/video/detail/reply/index.dart'; -import 'package:PiliPlus/pages/video/detail/controller.dart'; -import 'package:PiliPlus/pages/video/detail/introduction/index.dart'; -import 'package:PiliPlus/pages/video/detail/related/index.dart'; -import 'package:PiliPlus/plugin/pl_player/index.dart'; -import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; -import 'package:PiliPlus/services/service_locator.dart'; -import 'package:PiliPlus/utils/storage.dart'; -import 'package:PiliPlus/models/bangumi/info.dart' as bangumi; -import 'package:PiliPlus/models/video_detail_res.dart' as video; -import 'package:screen_brightness/screen_brightness.dart'; - -import '../../../services/shutdown_timer_service.dart'; -import 'widgets/header_control.dart'; -import 'package:PiliPlus/common/widgets/spring_physics.dart'; - -@Deprecated('Use VideoDetailPageV instead') -class VideoDetailPage extends StatefulWidget { - const VideoDetailPage({super.key}); - - @override - State createState() => _VideoDetailPageState(); - static final RouteObserver routeObserver = - RouteObserver(); -} - -class _VideoDetailPageState extends State - with TickerProviderStateMixin, RouteAware, WidgetsBindingObserver { - late VideoDetailController videoDetailController; - late VideoReplyController _videoReplyController; - PlPlayerController? plPlayerController; - late StreamController appbarStream; - late VideoIntroController videoIntroController; - late BangumiIntroController bangumiIntroController; - late final _introController = ScrollController(); - late String heroTag; - - double doubleOffset = 0; - - // 自动退出全屏 - late bool autoExitFullscreen; - late bool autoPlayEnable; - late bool horizontalScreen; - late bool enableVerticalExpand; - late bool autoPiP; - late bool pipNoDanmaku; - late bool removeSafeArea; - // late bool showStatusBarBackgroundColor; - // final Floating floating = Floating(); - // 生命周期监听 - // late final AppLifecycleListener _lifecycleListener; - bool isShowing = true; - // 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; - - bool get _horizontalPreview => - context.orientation == Orientation.landscape && - videoDetailController.horizontalPreview; - - StreamSubscription? _listenerDetail; - StreamSubscription? _listenerLoadingState; - StreamSubscription? _listenerCid; - - Box get setting => GStorage.setting; - - final GlobalKey relatedVideoPanelKey = GlobalKey(); - final GlobalKey videoPlayerKey = GlobalKey(); - final GlobalKey videoReplyPanelKey = GlobalKey(); - - @override - void initState() { - super.initState(); - - PlPlayerController.setPlayCallBack(playCallBack); - if (Get.arguments != null && Get.arguments['heroTag'] != null) { - heroTag = Get.arguments['heroTag']; - } - videoDetailController = Get.put(VideoDetailController(), tag: heroTag); - - if (videoDetailController.showReply) { - _videoReplyController = Get.put( - VideoReplyController(aid: videoDetailController.oid.value), - tag: heroTag, - ); - } - videoIntroController = Get.put(VideoIntroController(), tag: heroTag); - _listenerDetail = videoIntroController.videoDetail.listen((value) { - if (!context.mounted) return; - videoPlayerServiceHandler.onVideoDetailChange( - value, videoDetailController.cid.value, heroTag); - }); - if (videoDetailController.videoType == SearchType.media_bangumi) { - bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); - _listenerLoadingState = - bangumiIntroController.loadingState.listen((value) { - if (!context.mounted) return; - if (value is Success) { - videoPlayerServiceHandler.onVideoDetailChange( - value.response, videoDetailController.cid.value, heroTag); - } - }); - _listenerCid = videoDetailController.cid.listen((p0) { - if (!context.mounted) return; - if (bangumiIntroController.loadingState.value is Success) { - videoPlayerServiceHandler.onVideoDetailChange( - (bangumiIntroController.loadingState.value as Success).response, - p0, - heroTag); - } - }); - } - autoExitFullscreen = - setting.get(SettingBoxKey.enableAutoExit, defaultValue: true); - horizontalScreen = - setting.get(SettingBoxKey.horizontalScreen, defaultValue: false); - autoPlayEnable = - setting.get(SettingBoxKey.autoPlayEnable, defaultValue: false); - autoPiP = setting.get(SettingBoxKey.autoPiP, defaultValue: false); - pipNoDanmaku = setting.get(SettingBoxKey.pipNoDanmaku, defaultValue: false); - enableVerticalExpand = - setting.get(SettingBoxKey.enableVerticalExpand, defaultValue: false); - removeSafeArea = setting.get(SettingBoxKey.videoPlayerRemoveSafeArea, - defaultValue: false); - // showStatusBarBackgroundColor = setting.get( - // SettingBoxKey.videoPlayerShowStatusBarBackgroundColor, - // defaultValue: false); - if (removeSafeArea) hideStatusBar(); - videoSourceInit(); - appbarStreamListen(); - // lifecycleListener(); - autoScreen(); - if (Platform.isAndroid) { - Utils.channel.setMethodCallHandler((call) async { - if (call.method == 'onUserLeaveHint') { - if (autoPiP && - plPlayerController?.playerStatus.status.value == - PlayerStatus.playing) { - enterPip(); - } - } - }); - } - // _animationController = AnimationController( - // vsync: this, - // duration: const Duration(milliseconds: 300), - // ); - // _animation = Tween( - // begin: MediaQuery.of(context).orientation == Orientation.landscape - // ? context.height - // : ((enableVerticalExpand && - // videoDetailController.direction.value == 'vertical') - // ? context.width * 5 / 4 - // : context.width * 9 / 16), - // end: 0, - // ).animate(_animationController); - WidgetsBinding.instance.addObserver(this); - } - - // 获取视频资源,初始化播放器 - Future videoSourceInit() async { - videoDetailController.queryVideoUrl(); - if (videoDetailController.showReply) { - _videoReplyController.queryData(); - } - if (videoDetailController.autoPlay.value) { - plPlayerController = videoDetailController.plPlayerController; - plPlayerController!.addStatusLister(playerListener); - plPlayerController!.addPositionListener(positionListener); - await plPlayerController!.autoEnterFullscreen(); - } - } - - void positionListener(Duration position) { - videoDetailController.playedTime = position; - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - videoIntroController.startTimer(); - videoDetailController.plPlayerController.showDanmaku = true; - } else if (state == AppLifecycleState.paused) { - videoIntroController.canelTimer(); - videoDetailController.plPlayerController.showDanmaku = false; - } - } - - void playCallBack() { - plPlayerController?.play(); - } - - // 流 - appbarStreamListen() { - appbarStream = StreamController(); - } - - // 播放器状态监听 - void playerListener(PlayerStatus? status) async { - if (status == PlayerStatus.completed) { - try { - if ((videoDetailController.steinEdgeInfo?['edges']['questions'][0] - ['choices'] as List?) - ?.isNotEmpty == - true) { - videoDetailController.showSteinEdgeInfo.value = true; - return; - } - } catch (_) {} - shutdownTimerService.handleWaitingFinished(); - bool notExitFlag = false; - - /// 顺序播放 列表循环 - if (plPlayerController!.playRepeat != PlayRepeat.pause && - plPlayerController!.playRepeat != PlayRepeat.singleCycle) { - if (videoDetailController.isPlayAll || - videoDetailController.videoType == SearchType.video) { - notExitFlag = videoIntroController.nextPlay(); - } else if (videoDetailController.videoType == - SearchType.media_bangumi) { - notExitFlag = bangumiIntroController.nextPlay(); - } - } - - /// 单个循环 - if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) { - notExitFlag = true; - plPlayerController!.play(repeat: true); - } - - // 结束播放退出全屏 - if (!notExitFlag && autoExitFullscreen) { - plPlayerController!.triggerFullScreen(status: false); - if (plPlayerController!.longPressStatus.value) { - plPlayerController!.setLongPressStatus(false); - } - if (plPlayerController!.controlsLock.value) { - plPlayerController!.onLockControl(false); - } - } - // 播放完展示控制栏 - if (videoDetailController.floating != null && !notExitFlag) { - PiPStatus currentStatus = - await videoDetailController.floating!.pipStatus; - if (currentStatus == PiPStatus.disabled) { - plPlayerController!.onLockControl(false); - } - } - } - } - - // 继续播放或重新播放 - void continuePlay() async { - plPlayerController!.play(); - } - - /// 未开启自动播放时触发播放 - Future handlePlay() async { - if (videoDetailController.isQuerying) { - debugPrint('handlePlay: querying'); - return; - } - if (videoDetailController.videoUrl == null || - videoDetailController.audioUrl == null) { - debugPrint('handlePlay: videoUrl/audioUrl not initialized'); - videoDetailController.queryVideoUrl(); - return; - } - plPlayerController = videoDetailController.plPlayerController; - videoDetailController.isShowCover.value = false; - videoDetailController.autoPlay.value = true; - if (videoDetailController.preInitPlayer) { - await plPlayerController!.play(); - } else { - await videoDetailController.playerInit(autoplay: true); - } - plPlayerController!.addStatusLister(playerListener); - plPlayerController!.addPositionListener(positionListener); - await plPlayerController!.autoEnterFullscreen(); - } - - // // 生命周期监听 - // void lifecycleListener() { - // _lifecycleListener = AppLifecycleListener( - // onResume: () => _handleTransition('resume'), - // // 后台 - // onInactive: () => _handleTransition('inactive'), - // // 在Android和iOS端不生效 - // onHide: () => _handleTransition('hide'), - // onShow: () => _handleTransition('show'), - // onPause: () => _handleTransition('pause'), - // onRestart: () => _handleTransition('restart'), - // onDetach: () => _handleTransition('detach'), - // // 只作用于桌面端 - // onExitRequested: () { - // ScaffoldMessenger.maybeOf(context) - // ?.showSnackBar(const SnackBar(content: Text("拦截应用退出"))); - // return Future.value(AppExitResponse.cancel); - // }, - // ); - // } - - @override - void dispose() { - _listenerDetail?.cancel(); - _listenerLoadingState?.cancel(); - _listenerCid?.cancel(); - - videoDetailController.skipTimer?.cancel(); - videoDetailController.skipTimer = null; - - try { - Get.delete( - tag: videoDetailController.heroTag); - } catch (_) {} - - WidgetsBinding.instance.removeObserver(this); - if (!Get.previousRoute.startsWith('/video')) { - ScreenBrightness().resetApplicationScreenBrightness(); - PlPlayerController.setPlayCallBack(null); - } - videoDetailController.positionSubscription?.cancel(); - videoIntroController.canelTimer(); - appbarStream.close(); - // floating.dispose(); - // videoDetailController.floating?.dispose(); - videoIntroController.videoDetail.close(); - videoDetailController.cid.close(); - if (!horizontalScreen) { - AutoOrientation.portraitUpMode(); - } - shutdownTimerService.handleWaitingFinished(); - // _bufferedListener?.cancel(); - if (videoDetailController.plPlayerController.backToHome != true) { - videoPlayerServiceHandler.onVideoDetailDispose(heroTag); - } - if (plPlayerController != null) { - videoDetailController.makeHeartBeat(); - plPlayerController!.removeStatusLister(playerListener); - plPlayerController!.removePositionListener(positionListener); - plPlayerController!.dispose(); - } else { - PlPlayerController.updatePlayCount(); - } - VideoDetailPage.routeObserver.unsubscribe(this); - // _lifecycleListener.dispose(); - showStatusBar(); - // _animationController.dispose(); - super.dispose(); - } - - @override - // 离开当前页面时 - void didPushNext() async { - // _bufferedListener?.cancel(); - if (videoDetailController.imageStatus) { - return; - } - - WidgetsBinding.instance.removeObserver(this); - - ScreenBrightness().resetApplicationScreenBrightness(); - - videoDetailController.positionSubscription?.cancel(); - videoIntroController.canelTimer(); - - videoDetailController.playerStatus = - plPlayerController?.playerStatus.status.value; - - /// 开启 - // if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: true) - // as bool) { - videoDetailController.brightness = plPlayerController?.brightness.value; - // } - if (plPlayerController != null) { - videoDetailController.makeHeartBeat(); - videoDetailController.showVP = plPlayerController!.showVP.value; - plPlayerController!.removeStatusLister(playerListener); - plPlayerController!.removePositionListener(positionListener); - plPlayerController!.pause(); - } - isShowing = false; - super.didPushNext(); - } - - @override - // 返回当前页面时 - void didPopNext() async { - if (videoDetailController.imageStatus) { - return; - } - - WidgetsBinding.instance.addObserver(this); - - if (videoDetailController.plPlayerController.playerStatus.status.value == - PlayerStatus.playing && - videoDetailController.playerStatus != PlayerStatus.playing) { - videoDetailController.plPlayerController.pause(); - } - - isShowing = true; - PlPlayerController.setPlayCallBack(playCallBack); - videoIntroController.startTimer(); - if (mounted) { - if (videoDetailController.brightness != null) { - plPlayerController - ?.setCurrBrightness(videoDetailController.brightness!); - if (videoDetailController.brightness != -1.0) { - ScreenBrightness().setApplicationScreenBrightness( - videoDetailController.brightness!); - } else { - ScreenBrightness().resetApplicationScreenBrightness(); - } - } else { - ScreenBrightness().resetApplicationScreenBrightness(); - } - } - super.didPopNext(); - // final bool autoplay = autoPlayEnable; - videoDetailController.autoPlay.value = - !videoDetailController.isShowCover.value; - if (videoDetailController.isShowCover.value.not) { - await videoDetailController.playerInit( - autoplay: videoDetailController.playerStatus == PlayerStatus.playing, - ); - } else if (videoDetailController.preInitPlayer && - videoDetailController.isQuerying.not && - videoDetailController.videoState.value is! Error) { - await videoDetailController.playerInit(); - } - - // if (videoDetailController.playerStatus == PlayerStatus.playing) { - // plPlayerController?.play(); - // } - - /// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回 - // if (autoplay) { - // // await Future.delayed(const Duration(milliseconds: 300)); - // debugPrint(plPlayerController); - // if (plPlayerController?.buffered.value == Duration.zero) { - // _bufferedListener = plPlayerController?.buffered.listen((p0) { - // debugPrint("p0"); - // debugPrint(p0); - // if (p0 > Duration.zero) { - // _bufferedListener!.cancel(); - // plPlayerController?.seekTo(videoDetailController.defaultST); - // plPlayerController?.play(); - // } - // }); - // } else { - // plPlayerController?.seekTo(videoDetailController.defaultST); - // plPlayerController?.play(); - // } - // } - Future.delayed(const Duration(milliseconds: 600), () { - AutoOrientation.fullAutoMode(); - }); - plPlayerController?.addStatusLister(playerListener); - plPlayerController?.addPositionListener(positionListener); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - VideoDetailPage.routeObserver - .subscribe(this, ModalRoute.of(context)! as PageRoute); - themeData = videoDetailController.plPlayerController.darkVideoPage - ? MyApp.darkThemeData ?? Theme.of(context) - : Theme.of(context); - } - - // void _handleTransition(String name) { - // switch (name) { - // case 'inactive': - // if (plPlayerController != null && - // playerStatus == PlayerStatus.playing) { - // autoEnterPip(); - // } - // break; - // } - // } - - void enterPip() { - if (Get.currentRoute.startsWith('/video') && - videoDetailController.floating != null) { - Utils.enterPip( - videoDetailController.floating!, - videoDetailController.data.dash!.video!.first.width!, - videoDetailController.data.dash!.video!.first.height!, - ); - } - } - - Widget get childWhenDisabled => SafeArea( - top: !removeSafeArea && - MediaQuery.of(context).orientation == Orientation.portrait && - isFullScreen, - bottom: !removeSafeArea && - MediaQuery.of(context).orientation == Orientation.portrait && - isFullScreen, - left: false, //!isFullScreen, - right: false, //!isFullScreen, - child: Stack( - children: [ - Scaffold( - resizeToAvoidBottomInset: false, - key: videoDetailController.scaffoldKey, - appBar: removeSafeArea - ? null - : AppBar( - backgroundColor: Colors.black, - toolbarHeight: 0, - ), - body: Column( - children: [ - Obx( - () { - double videoHeight = context.width * 9 / 16; - final double videoWidth = context.width; - // debugPrint(videoDetailController.tabCtr.index); - if (enableVerticalExpand && - videoDetailController.direction.value == 'vertical') { - videoHeight = context.width; - } - if (MediaQuery.of(context).orientation == - Orientation.landscape && - !horizontalScreen && - !isFullScreen && - isShowing && - mounted) { - hideStatusBar(); - } - if (MediaQuery.of(context).orientation == - Orientation.portrait && - !isFullScreen && - isShowing && - mounted) { - if (videoDetailController.imageStatus.not && - removeSafeArea.not) { - showStatusBar(); - } - } - return SizedBox( - height: MediaQuery.of(context).orientation == - Orientation.landscape || - isFullScreen - ? MediaQuery.sizeOf(context).height - - (MediaQuery.of(context).orientation == - Orientation.landscape || - removeSafeArea - ? 0 - : MediaQuery.of(context).padding.top) - : videoHeight, - width: context.width, - child: videoPlayer(videoWidth, videoHeight), - ); - }, - ), - Expanded( - child: Scaffold( - key: videoDetailController.childKey, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Column( - children: [ - buildTabbar( - showReply: videoDetailController.showReply, - ), - Expanded( - child: videoTabBarView( - controller: videoDetailController.tabCtr, - children: [ - videoIntro(), - if (videoDetailController.showReply) - videoReplyPanel, - if (_shouldShowSeasonPanel) seasonPanel, - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ); - - Widget get childWhenDisabledAlmostSquareInner => Obx( - () { - if (enableVerticalExpand && - videoDetailController.direction.value == 'vertical') { - final double videoHeight = context.height - - (removeSafeArea - ? 0 - : (MediaQuery.of(context).padding.top + - MediaQuery.of(context).padding.bottom)); - final double videoWidth = videoHeight * 9 / 16; - return Row( - children: [ - SizedBox( - height: videoHeight, - width: isFullScreen ? context.width : videoWidth, - child: videoPlayer(videoWidth, videoHeight), - ), - Expanded( - child: Scaffold( - key: videoDetailController.childKey, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Column( - children: [ - buildTabbar(showReply: videoDetailController.showReply), - Expanded( - child: videoTabBarView( - controller: videoDetailController.tabCtr, - children: [ - videoIntro(), - if (videoDetailController.showReply) - videoReplyPanel, - if (_shouldShowSeasonPanel) seasonPanel, - ], - ), - ), - ], - ), - ), - ), - ], - ); - } - final double videoHeight = context.height / 2.5; - final double videoWidth = context.width; - return Column( - children: [ - SizedBox( - width: videoWidth, - height: isFullScreen - ? context.height - - (removeSafeArea - ? 0 - : (MediaQuery.of(context).padding.top + - MediaQuery.of(context).padding.bottom)) - : videoHeight, - child: videoPlayer(videoWidth, videoHeight), - ), - Expanded( - child: Scaffold( - key: videoDetailController.childKey, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Column( - children: [ - buildTabbar( - needIndicator: false, - showReply: videoDetailController.showReply, - ), - Expanded( - child: Row( - children: [ - Expanded(child: videoIntro()), - if (videoDetailController.showReply) - Expanded(child: videoReplyPanel), - if (_shouldShowSeasonPanel) - Expanded(child: seasonPanel), - ], - ), - ) - ], - ), - ), - ), - ], - ); - }, - ); - - Widget get childWhenDisabledLandscapeInner => Obx( - () { - if (enableVerticalExpand && - videoDetailController.direction.value == 'vertical') { - final double videoHeight = context.height - - (removeSafeArea ? 0 : MediaQuery.of(context).padding.top); - final double videoWidth = videoHeight * 9 / 16; - return Row( - children: [ - Expanded( - child: - isFullScreen ? const SizedBox.shrink() : videoIntro()), - SizedBox( - height: videoHeight, - width: isFullScreen ? context.width : videoWidth, - child: videoPlayer(videoWidth, videoHeight), - ), - Expanded( - child: Expanded( - child: Scaffold( - key: videoDetailController.childKey, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Column( - children: [ - buildTabbar( - showIntro: false, - showReply: videoDetailController.showReply, - ), - Expanded( - child: videoTabBarView( - controller: videoDetailController.tabCtr, - children: [ - if (videoDetailController.showReply) - videoReplyPanel, - if (_shouldShowSeasonPanel) seasonPanel, - ], - ), - ), - ], - ), - ), - ), - ), - // Expanded( - // child: TabBarView( - // physics: const ClampingScrollPhysics(), - // controller: videoDetailController.tabCtr, - // children: [ - // CustomScrollView( - // key: const PageStorageKey('简介'), - // slivers: [ - // if (videoDetailController.videoType == - // SearchType.video) ...[ - // const VideoIntroPanel(), - // ] else if (videoDetailController.videoType == - // SearchType.media_bangumi) ...[ - // Obx(() => BangumiIntroPanel( - // cid: videoDetailController.cid.value)), - // ], - // SliverToBoxAdapter( - // child: Divider( - // indent: 12, - // endIndent: 12, - // color: themeData.dividerColor.withOpacity(0.06), - // ), - // ), - // const RelatedVideoPanel(), - // ], - // ), - // Obx( - // () => VideoReplyPanel( - // bvid: videoDetailController.bvid, - // oid: videoDetailController.oid.value, - // ), - // ) - // ], - // ), - // ), - ], - ); - } - double videoWidth = - max(context.height / context.width * 1.04, 1 / 2) * context.width; - if (context.width >= 560) { - videoWidth = min(videoWidth, context.width - 280); - } - final double videoHeight = videoWidth * 9 / 16; - return Row( - children: [ - Column( - children: [ - SizedBox( - width: isFullScreen ? context.width : videoWidth, - height: isFullScreen ? context.height : videoHeight, - child: videoPlayer(videoWidth, videoHeight), - ), - Offstage( - offstage: isFullScreen, - child: SizedBox( - width: videoWidth, - height: context.height - - videoHeight - - (removeSafeArea - ? 0 - : MediaQuery.of(context).padding.top), - child: videoIntro(false), - ), - ), - ], - ), - Offstage( - offstage: isFullScreen, - child: SizedBox( - width: (context.width - - videoWidth - - (removeSafeArea - ? 0 - : (MediaQuery.of(context).padding.left + - MediaQuery.of(context).padding.right))), - height: context.height - - (removeSafeArea ? 0 : MediaQuery.of(context).padding.top), - child: Scaffold( - key: videoDetailController.childKey, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Column( - children: [ - buildTabbar( - introText: '相关视频', - showIntro: videoDetailController.videoType == - SearchType.video && - videoDetailController.showRelatedVideo, - showReply: videoDetailController.showReply, - ), - Expanded( - child: videoTabBarView( - controller: videoDetailController.tabCtr, - children: [ - if (videoDetailController.videoType == - SearchType.video && - videoDetailController.showRelatedVideo) - CustomScrollView( - controller: _introController, - slivers: [ - RelatedVideoPanel( - key: relatedVideoPanelKey, - heroTag: heroTag), - ], - ), - if (videoDetailController.showReply) - videoReplyPanel, - if (_shouldShowSeasonPanel) seasonPanel, - ], - ), - ), - ], - ), - ), - ), - ) - ], - ); - }, - ); - - Widget get childWhenDisabledLandscape => Stack( - children: [ - Scaffold( - resizeToAvoidBottomInset: false, - key: videoDetailController.scaffoldKey, - appBar: removeSafeArea - ? null - : AppBar( - backgroundColor: Colors.black, - toolbarHeight: 0, - ), - body: SafeArea( - left: !removeSafeArea && !isFullScreen, - right: !removeSafeArea && !isFullScreen, - top: !removeSafeArea, - bottom: false, //!removeSafeArea, - child: childWhenDisabledLandscapeInner, - ), - ), - ], - ); - - Widget get childWhenDisabledAlmostSquare => Scaffold( - resizeToAvoidBottomInset: false, - key: videoDetailController.scaffoldKey, - appBar: removeSafeArea - ? null - : AppBar( - backgroundColor: Colors.black, - toolbarHeight: 0, - ), - body: SafeArea( - left: !removeSafeArea && !isFullScreen, - right: !removeSafeArea && !isFullScreen, - top: !removeSafeArea, - bottom: false, //!removeSafeArea, - child: childWhenDisabledAlmostSquareInner, - ), - ); - - Widget get childWhenEnabled => Obx( - () => !videoDetailController.autoPlay.value - ? const SizedBox() - : PLVideoPlayer( - key: Key(heroTag), - plPlayerController: plPlayerController!, - videoDetailController: videoDetailController, - videoIntroController: - videoDetailController.videoType == SearchType.video - ? videoIntroController - : null, - bangumiIntroController: - videoDetailController.videoType == SearchType.media_bangumi - ? bangumiIntroController - : null, - headerControl: HeaderControl( - controller: plPlayerController!, - videoDetailCtr: videoDetailController, - heroTag: heroTag, - ), - danmuWidget: pipNoDanmaku - ? null - : Obx( - () => PlDanmaku( - key: Key(videoDetailController.danmakuCid.value - .toString()), - isPipMode: true, - cid: videoDetailController.danmakuCid.value, - playerController: plPlayerController!, - ), - ), - showEpisodes: showEpisodes, - showViewPoints: showViewPoints, - ), - ); - - Widget get manualPlayerWidget => Obx( - () => Visibility( - visible: videoDetailController.isShowCover.value, - child: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: AppBar( - elevation: 0, - scrolledUnderElevation: 0, - primary: false, - foregroundColor: Colors.white, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - title: Row( - children: [ - SizedBox( - width: 42, - height: 34, - child: IconButton( - tooltip: '返回', - icon: const Icon( - FontAwesomeIcons.arrowLeft, - size: 15, - color: Colors.white, - shadows: [ - Shadow( - blurRadius: 1.5, - color: Colors.black, - ), - ], - ), - onPressed: Get.back, - ), - ), - SizedBox( - width: 42, - height: 34, - child: IconButton( - tooltip: '返回主页', - icon: const Icon( - FontAwesomeIcons.house, - size: 15, - color: Colors.white, - shadows: [ - Shadow( - blurRadius: 1.5, - color: Colors.black, - ), - ], - ), - onPressed: () { - videoDetailController - .plPlayerController.backToHome = true; - Get.until((route) => route.isFirst); - }, - ), - ), - ], - ), - actions: [ - PopupMenuButton( - icon: const Icon( - Icons.more_vert, - color: Colors.white, - shadows: [ - Shadow( - blurRadius: 1.5, - color: Colors.black, - ), - ], - ), - onSelected: (String type) async { - switch (type) { - case 'later': - await videoIntroController.viewLater(); - break; - case 'report': - if (!Accounts.main.isLogin) { - SmartDialog.showToast('账号未登录'); - } else { - Get.toNamed('/webview', parameters: { - 'url': - 'https://www.bilibili.com/appeal/?avid=${IdUtils.bv2av(videoDetailController.bvid)}&bvid=${videoDetailController.bvid}' - }); - } - break; - case 'note': - videoDetailController.showNoteList(context); - break; - case 'savePic': - DownloadUtils.downloadImg( - context, - [videoDetailController.videoItem['pic']], - ); - break; - } - }, - itemBuilder: (BuildContext context) => - >[ - const PopupMenuItem( - value: 'later', - child: Text('稍后再看'), - ), - if (videoDetailController.epId == null) - const PopupMenuItem( - value: 'note', - child: Text('查看笔记'), - ), - if (videoDetailController.videoItem['pic'] != null) - const PopupMenuItem( - value: 'savePic', - child: Text('保存封面'), - ), - const PopupMenuItem( - value: 'report', - child: Text('举报'), - ), - ], - ), - ], - ), - ), - Positioned( - right: 12, - bottom: 10, - child: IconButton( - tooltip: '播放', - onPressed: handlePlay, - icon: Image.asset( - 'assets/images/play.png', - width: 60, - height: 60, - ), - ), - ), - ], - ), - ), - ); - - Widget get plPlayer => Obx( - key: videoPlayerKey, - () => videoDetailController.videoState.value is! Success - ? const SizedBox.shrink() - : !videoDetailController.autoPlay.value || - plPlayerController?.videoController == null - ? const SizedBox.shrink() - : PLVideoPlayer( - key: Key(heroTag), - plPlayerController: plPlayerController!, - videoDetailController: videoDetailController, - videoIntroController: - videoDetailController.videoType == SearchType.video - ? videoIntroController - : null, - bangumiIntroController: videoDetailController.videoType == - SearchType.media_bangumi - ? bangumiIntroController - : null, - headerControl: videoDetailController.headerControl, - danmuWidget: Obx( - () => PlDanmaku( - key: Key( - videoDetailController.danmakuCid.value.toString()), - cid: videoDetailController.danmakuCid.value, - playerController: plPlayerController!, - ), - ), - showEpisodes: showEpisodes, - showViewPoints: showViewPoints, - ), - ); - - Widget autoChoose(Widget childWhenDisabled) { - if (Platform.isAndroid) { - return PiPSwitcher( - getChildWhenDisabled: () => childWhenDisabled, - getChildWhenEnabled: () => childWhenEnabled, - floating: videoDetailController.floating, - ); - } - return childWhenDisabled; - } - - late ThemeData themeData; - - Widget get child { - if (!horizontalScreen) { - return autoChoose(childWhenDisabled); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // if (!isShowing) { - // return ColoredBox(color: themeData.colorScheme.surface); - // } - if (constraints.maxWidth > constraints.maxHeight * 1.25) { -// hideStatusBar(); -// videoDetailController.hiddenReplyReplyPanel(); - return autoChoose(childWhenDisabledLandscape); - } else if (constraints.maxWidth * (9 / 16) < - (2 / 5) * constraints.maxHeight) { - // if (!isFullScreen) { - // if (!removeSafeArea) showStatusBar(); - // } - return autoChoose(childWhenDisabled); - } else { - // if (!isFullScreen) { - // if (!removeSafeArea) showStatusBar(); - // } - return autoChoose(childWhenDisabledAlmostSquare); - } - // - // final Orientation orientation = - // constraints.maxWidth > constraints.maxHeight * 1.25 - // ? Orientation.landscape - // : Orientation.portrait; - // if (orientation == Orientation.landscape) { - // if (!horizontalScreen) { - // hideStatusBar(); - // videoDetailController.hiddenReplyReplyPanel(); - // } - // } else { - // if (!isFullScreen) { - // showStatusBar(); - // } - // } - // if (Platform.isAndroid) { - // return PiPSwitcher( - // childWhenDisabled: - // !horizontalScreen || orientation == Orientation.portrait - // ? childWhenDisabled - // : childWhenDisabledLandscape, - // childWhenEnabled: childWhenEnabled, - // floating: floating, - // ); - // } - // return !horizontalScreen || orientation == Orientation.portrait - // ? childWhenDisabled - // : childWhenDisabledLandscape; - }, - ); - } - - @override - Widget build(BuildContext context) { - return videoDetailController.plPlayerController.darkVideoPage - ? Theme(data: themeData, child: child) - : child; - } - - Widget buildTabbar({ - bool needIndicator = true, - String introText = '简介', - bool showIntro = true, - bool showReply = true, - }) { - List tabs = [ - if (showIntro) introText, - if (showReply) '评论', - if (_shouldShowSeasonPanel) '播放列表', - ]; - if (videoDetailController.tabCtr.length != tabs.length) { - videoDetailController.tabCtr = TabController( - vsync: this, - length: tabs.length, - initialIndex: - videoDetailController.tabCtr.index.clamp(0, tabs.length - 1), - ); - } - - Widget tabbar() => TabBar( - labelColor: needIndicator.not || tabs.length == 1 - ? themeData.colorScheme.onSurface - : null, - indicator: needIndicator.not || tabs.length == 1 - ? const BoxDecoration() - : null, - padding: EdgeInsets.zero, - controller: videoDetailController.tabCtr, - labelStyle: - TabBarTheme.of(context).labelStyle?.copyWith(fontSize: 13) ?? - const TextStyle(fontSize: 13), - labelPadding: - const EdgeInsets.symmetric(horizontal: 10.0), // 设置每个标签的宽度 - dividerColor: Colors.transparent, - dividerHeight: 0, - onTap: (value) { - void animToTop() { - String text = tabs[value]; - if (text == '简介' || text == '相关视频') { - _introController.animToTop(); - } else if (text.startsWith('评论')) { - _videoReplyController.animateToTop(); - } - } - - if (needIndicator.not || tabs.length == 1) { - animToTop(); - } else if (videoDetailController.tabCtr.indexIsChanging.not) { - animToTop(); - } - }, - tabs: tabs.map((text) { - if (text == '评论') { - return Tab( - text: - '评论${_videoReplyController.count.value == -1 ? '' : ' ${Utils.numFormat(_videoReplyController.count.value)}'}', - ); - } else { - return Tab(text: text); - } - }).toList(), - ); - - return Container( - width: double.infinity, - height: 45, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1, - color: themeData.dividerColor.withOpacity(0.1), - ), - ), - ), - child: Row( - children: [ - if (tabs.isEmpty) - const Spacer() - else - Flexible( - flex: tabs.length == 3 ? 2 : 1, - child: showReply ? Obx(() => tabbar()) : tabbar(), - ), - Flexible( - flex: 1, - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - height: 32, - child: TextButton( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero), - ), - onPressed: videoDetailController.showShootDanmakuSheet, - child: Text( - '发弹幕', - style: TextStyle( - fontSize: 12, - color: themeData.colorScheme.onSurfaceVariant, - ), - ), - ), - ), - SizedBox( - width: 38, - height: 38, - child: Obx( - () => IconButton( - onPressed: () { - videoDetailController - .plPlayerController.isOpenDanmu.value = - !videoDetailController - .plPlayerController.isOpenDanmu.value; - setting.put( - SettingBoxKey.enableShowDanmaku, - videoDetailController - .plPlayerController.isOpenDanmu.value); - }, - icon: SvgPicture.asset( - videoDetailController - .plPlayerController.isOpenDanmu.value - ? 'assets/images/video/danmu_open.svg' - : 'assets/images/video/danmu_close.svg', - // ignore: deprecated_member_use - color: videoDetailController - .plPlayerController.isOpenDanmu.value - ? themeData.colorScheme.secondary - : themeData.colorScheme.outline, - ), - ), - ), - ), - const SizedBox(width: 14), - ], - ), - ), - ), - ], - ), - ); - } - - Widget videoPlayer(double videoWidth, double videoHeight) => PopScope( - canPop: !isFullScreen && - (horizontalScreen || - MediaQuery.of(context).orientation == Orientation.portrait), - onPopInvokedWithResult: _onPopInvokedWithResult, - child: Stack( - children: [ - Positioned.fill(child: ColoredBox(color: Colors.black)), - - if (isShowing) plPlayer, - - /// 关闭自动播放时 手动播放 - if (!videoDetailController.autoPlay.value) ...[ - Obx( - () => Visibility( - visible: videoDetailController.isShowCover.value, - child: Positioned( - top: 0, - left: 0, - right: 0, - child: GestureDetector( - onTap: handlePlay, - child: Obx( - () => NetworkImgLayer( - type: 'emote', - src: videoDetailController.videoItem['pic'], - width: videoWidth, - height: videoHeight, - ), - ), - ), - ), - ), - ), - manualPlayerWidget, - ], - - if (videoDetailController.enableSponsorBlock || - videoDetailController.continuePlayingPart) - Positioned( - left: 16, - bottom: isFullScreen ? max(75, Get.height * 0.25) : 75, - child: SizedBox( - width: MediaQuery.textScalerOf(context).scale(120), - child: AnimatedList( - padding: EdgeInsets.zero, - key: videoDetailController.listKey, - reverse: true, - shrinkWrap: true, - initialItemCount: videoDetailController.listData.length, - itemBuilder: (context, index, animation) { - return videoDetailController.buildItem( - videoDetailController.listData[index], - animation, - ); - }, - ), - ), - ), - - // for debug - // Positioned( - // right: 16, - // bottom: 75, - // child: FilledButton.tonal( - // onPressed: () { - // videoDetailController.onAddItem( - // SegmentModel( - // UUID: '', - // segmentType: SegmentType.sponsor, - // segment: Pair(first: 0, second: 0), - // skipType: SkipType.alwaysSkip, - // ), - // ); - // }, - // child: Text('skip'), - // ), - // ), - // Positioned( - // right: 16, - // bottom: 120, - // child: FilledButton.tonal( - // onPressed: () { - // videoDetailController.onAddItem(2); - // }, - // child: Text('index'), - // ), - // ), - - Obx( - () { - if (videoDetailController.showSteinEdgeInfo.value) { - try { - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: - plPlayerController?.showControls.value == true - ? 75 - : 16, - ), - child: Wrap( - spacing: 25, - runSpacing: 10, - children: - (videoDetailController.steinEdgeInfo!['edges'] - ['questions'][0]['choices'] as List) - .map((item) { - return FilledButton.tonal( - style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - backgroundColor: themeData - .colorScheme.secondaryContainer - .withOpacity(0.8), - padding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 10, - ), - visualDensity: - VisualDensity(horizontal: -2, vertical: -2), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - onPressed: () { - videoIntroController.changeSeasonOrbangu( - null, - videoDetailController.bvid, - item['cid'], - IdUtils.bv2av(videoDetailController.bvid), - null, - true, - ); - videoDetailController - .getSteinEdgeInfo(item['id']); - }, - child: Text(item['option']), - ); - }).toList(), - ), - ), - ); - } catch (e) { - debugPrint('build stein edges: $e'); - return const SizedBox.shrink(); - } - } - return const SizedBox.shrink(); - }, - ), - ], - ), - ); - - Widget videoIntro([bool needRelated = true]) { - Widget introPanel() => CustomScrollView( - key: const PageStorageKey('简介'), - controller: needRelated ? _introController : null, - slivers: [ - if (videoDetailController.videoType == SearchType.video) ...[ - VideoIntroPanel( - heroTag: heroTag, - showAiBottomSheet: showAiBottomSheet, - showEpisodes: showEpisodes, - onShowMemberPage: onShowMemberPage, - ), - if (needRelated && videoDetailController.showRelatedVideo) ...[ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: StyleString.safeSpace), - child: Divider( - height: 1, - indent: 12, - endIndent: 12, - color: themeData.colorScheme.outline.withOpacity(0.08), - ), - ), - ), - RelatedVideoPanel(key: relatedVideoPanelKey, heroTag: heroTag), - ] else - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.paddingOf(context).bottom + - StyleString.safeSpace, - ), - ), - ] else if (videoDetailController.videoType == - SearchType.media_bangumi) - Obx( - () => BangumiIntroPanel( - heroTag: heroTag, - cid: videoDetailController.cid.value, - showEpisodes: showEpisodes, - showIntroDetail: showIntroDetail, - ), - ), - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.paddingOf(context).bottom + - (videoDetailController.isPlayAll && - MediaQuery.orientationOf(context) == - Orientation.landscape - ? 75 - : 0), - ), - ) - ], - ); - if (videoDetailController.isPlayAll) { - return Stack( - children: [ - introPanel(), - Positioned( - left: 12, - right: 12, - bottom: MediaQuery.of(context).padding.bottom + 12, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => videoDetailController.showMediaListPanel(context), - borderRadius: const BorderRadius.all(Radius.circular(14)), - child: Container( - height: 54, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: themeData.colorScheme.secondaryContainer - .withOpacity(0.95), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - child: Row( - children: [ - const Icon(Icons.playlist_play, size: 24), - const SizedBox(width: 10), - Text( - videoDetailController.watchLaterTitle, - style: TextStyle( - color: themeData.colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - letterSpacing: 0.2, - ), - ), - const Spacer(), - const Icon(Icons.keyboard_arrow_up_rounded, size: 26), - ], - ), - ), - ), - ), - ), - ], - ); - } else { - return introPanel(); - } - } - - Widget get seasonPanel => Column( - children: [ - if ((videoIntroController.videoDetail.value.pages?.length ?? 0) > 1) - if (videoIntroController.videoDetail.value.ugcSeason != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: PagesPanel( - heroTag: heroTag, - videoIntroController: videoIntroController, - bvid: videoIntroController.bvid, - changeFuc: videoIntroController.changeSeasonOrbangu, - showEpisodes: showEpisodes, - ), - ) - else - Expanded( - child: Obx( - () => ListSheetContent( - episodes: videoIntroController.videoDetail.value.pages, - bvid: videoDetailController.bvid, - aid: IdUtils.bv2av(videoDetailController.bvid), - currentCid: videoDetailController.cid.value, - isReversed: - videoIntroController.videoDetail.value.isPageReversed, - changeFucCall: videoDetailController.videoType == - SearchType.media_bangumi - ? bangumiIntroController.changeSeasonOrbangu - : videoIntroController.changeSeasonOrbangu, - showTitle: false, - isSupportReverse: videoDetailController.videoType != - SearchType.media_bangumi, - onReverse: () { - onReversePlay( - bvid: videoDetailController.bvid, - aid: IdUtils.bv2av(videoDetailController.bvid), - isSeason: false, - ); - }, - ), - ), - ), - if (videoIntroController.videoDetail.value.ugcSeason != null) ...[ - if ((videoIntroController.videoDetail.value.pages?.length ?? 0) > - 1) ...[ - const SizedBox(height: 8), - Divider( - height: 1, - color: themeData.colorScheme.outline.withOpacity(0.1), - ), - ], - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: SeasonPanel( - heroTag: heroTag, - onTap: false, - changeFuc: videoIntroController.changeSeasonOrbangu, - showEpisodes: showEpisodes, - videoIntroController: videoIntroController, - ), - ), - Expanded( - child: Obx( - () => ListSheetContent( - index: videoDetailController.seasonIndex.value, - season: videoIntroController.videoDetail.value.ugcSeason, - bvid: videoDetailController.bvid, - aid: IdUtils.bv2av(videoDetailController.bvid), - currentCid: videoDetailController.seasonCid ?? 0, - isReversed: - videoIntroController.videoDetail.value.isSeasonReversed, - changeFucCall: videoDetailController.videoType == - SearchType.media_bangumi - ? bangumiIntroController.changeSeasonOrbangu - : videoIntroController.changeSeasonOrbangu, - showTitle: false, - isSupportReverse: videoDetailController.videoType != - SearchType.media_bangumi, - onReverse: () { - onReversePlay( - bvid: videoDetailController.bvid, - aid: IdUtils.bv2av(videoDetailController.bvid), - isSeason: true, - ); - }, - ), - ), - ), - ], - ], - ); - - Widget get videoReplyPanel => Obx( - () => VideoReplyPanel( - key: videoReplyPanelKey, - bvid: videoDetailController.bvid, - oid: videoDetailController.oid.value, - heroTag: heroTag, - replyReply: replyReply, - onViewImage: videoDetailController.onViewImage, - onDismissed: videoDetailController.onDismissed, - callback: _horizontalPreview - ? (imgList, index) { - final ctr = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - )..forward(); - Utils.onHorizontalPreview( - videoDetailController.childKey, - AnimationController( - vsync: this, - duration: Duration.zero, - ), - ctr, - imgList, - index, - (value) async { - if (value == false) { - await ctr.reverse(); - } - try { - ctr.dispose(); - } catch (_) {} - if (value == false) { - Get.back(); - } - }, - ); - } - : null, - ), - ); - - // 展示二级回复 - void replyReply(replyItem, id, isTop) { - EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () { - int oid = replyItem.oid.toInt(); - int rpid = replyItem.id.toInt(); - videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Colors.transparent, - (context) => VideoReplyReplyPanel( - id: id, - oid: oid, - rpid: rpid, - firstFloor: replyItem, - replyType: ReplyType.video, - source: 'videoDetail', - isTop: isTop ?? false, - onViewImage: videoDetailController.onViewImage, - onDismissed: videoDetailController.onDismissed, - ), - ); - }); - } - - // ai总结 - showAiBottomSheet() { - videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Colors.transparent, - (context) => AiDetail(modelResult: videoIntroController.modelResult), - ); - } - - showIntroDetail(videoDetail, videoTags) { - videoDetailController.childKey.currentState?.showBottomSheet( - shape: const RoundedRectangleBorder(), - backgroundColor: themeData.colorScheme.surface, - (context) => bangumi.IntroDetail( - bangumiDetail: videoDetail, - videoTags: videoTags, - ), - ); - } - - showEpisodes([index, season, episodes, bvid, aid, cid]) { - if (bvid == null) { - videoDetailController.showMediaListPanel(context); - return; - } - Widget listSheetContent([bool? enableSlide]) => ListSheetContent( - enableSlide: enableSlide, - index: index, - season: season, - bvid: bvid, - aid: aid, - currentCid: cid, - episodes: episodes, - isReversed: - videoDetailController.videoType == SearchType.media_bangumi - ? null - : season != null - ? videoIntroController.videoDetail.value.isSeasonReversed - : videoIntroController.videoDetail.value.isPageReversed, - isSupportReverse: - videoDetailController.videoType != SearchType.media_bangumi, - changeFucCall: - videoDetailController.videoType == SearchType.media_bangumi - ? bangumiIntroController.changeSeasonOrbangu - : videoIntroController.changeSeasonOrbangu, - onClose: Get.back, - onReverse: () { - Get.back(); - onReversePlay( - bvid: bvid, - aid: aid, - isSeason: season != null, - ); - }, - ); - if (isFullScreen) { - Utils.showFSSheet( - context, - child: Theme( - data: themeData, - child: listSheetContent(false), - ), - isFullScreen: () => isFullScreen, - ); - } else { - videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Colors.transparent, - (context) => listSheetContent(), - ); - } - } - - void onReversePlay({ - required dynamic bvid, - required dynamic aid, - required bool isSeason, - }) { - void changeEpisode(episode) { - videoIntroController.changeSeasonOrbangu( - episode is bangumi.EpisodeItem ? episode.epId : null, - episode.runtimeType.toString() == "EpisodeItem" ? episode.bvid : bvid, - episode.cid, - episode.runtimeType.toString() == "EpisodeItem" ? episode.aid : aid, - episode is video.EpisodeItem - ? episode.arc?.pic - : episode is bangumi.EpisodeItem - ? episode.cover - : null, - ); - } - - if (isSeason) { - // reverse season - videoIntroController.videoDetail.value.isSeasonReversed = - !videoIntroController.videoDetail.value.isSeasonReversed; - videoIntroController.videoDetail.value.ugcSeason! - .sections![videoDetailController.seasonIndex.value].episodes = - videoIntroController - .videoDetail - .value - .ugcSeason! - .sections![videoDetailController.seasonIndex.value] - .episodes! - .reversed - .toList(); - - if (videoDetailController.reverseFromFirst.not) { - // keep current episode - videoDetailController.seasonIndex.refresh(); - videoDetailController.cid.refresh(); - } else { - // switch to first episode - dynamic episode = videoIntroController.videoDetail.value.ugcSeason! - .sections![videoDetailController.seasonIndex.value].episodes!.first; - if (episode.cid != videoDetailController.cid.value) { - changeEpisode(episode); - } else { - videoDetailController.seasonIndex.refresh(); - videoDetailController.cid.refresh(); - } - } - } else { - // reverse part - videoIntroController.videoDetail.value.isPageReversed = - !videoIntroController.videoDetail.value.isPageReversed; - videoIntroController.videoDetail.value.pages = - videoIntroController.videoDetail.value.pages!.reversed.toList(); - if (videoDetailController.reverseFromFirst.not) { - // keep current episode - videoDetailController.cid.refresh(); - } else { - // switch to first episode - dynamic episode = videoIntroController.videoDetail.value.pages!.first; - if (episode.cid != videoDetailController.cid.value) { - changeEpisode(episode); - } else { - videoDetailController.cid.refresh(); - } - } - } - } - - void showViewPoints() { - if (isFullScreen) { - Utils.showFSSheet( - context, - child: Theme( - data: themeData, - child: ViewPointsPage( - enableSlide: false, - videoDetailController: videoDetailController, - plPlayerController: plPlayerController, - ), - ), - isFullScreen: () => isFullScreen, - ); - } else { - videoDetailController.childKey.currentState?.showBottomSheet( - backgroundColor: Colors.transparent, - (context) => ViewPointsPage( - videoDetailController: videoDetailController, - plPlayerController: plPlayerController, - ), - ); - } - } - - void _onPopInvokedWithResult(didPop, result) { - if (plPlayerController?.controlsLock.value == true) { - plPlayerController?.onLockControl(false); - return; - } - if (isFullScreen) { - plPlayerController!.triggerFullScreen(status: false); - } - if (MediaQuery.of(context).orientation == Orientation.landscape && - !horizontalScreen) { - verticalScreenForTwoSeconds(); - } - } - - void onShowMemberPage(mid) { - videoDetailController.childKey.currentState?.showBottomSheet( - shape: const RoundedRectangleBorder(), - backgroundColor: themeData.colorScheme.surface, - (context) { - return HorizontalMemberPage( - mid: mid, - videoDetailController: videoDetailController, - videoIntroController: videoIntroController, - ); - }, - ); - } -} diff --git a/lib/pages/video/detail/view_v.dart b/lib/pages/video/detail/view_v.dart index a2ea51029..4a1d024e7 100644 --- a/lib/pages/video/detail/view_v.dart +++ b/lib/pages/video/detail/view_v.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'dart:ui'; import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/list_sheet.dart'; +import 'package:PiliPlus/common/widgets/episode_panel.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/reply_type.dart'; @@ -2075,18 +2075,23 @@ class _VideoDetailPageVState extends State heroTag: heroTag, videoIntroController: videoIntroController, bvid: videoIntroController.bvid, - changeFuc: videoIntroController.changeSeasonOrbangu, showEpisodes: showEpisodes, ), ) else Expanded( child: Obx( - () => ListSheetContent( - episodes: videoIntroController.videoDetail.value.pages, + () => EpisodePanel( + heroTag: heroTag, + videoIntroController: videoIntroController, + type: EpisodeType.part, + list: [videoIntroController.videoDetail.value.pages!], + cover: videoDetailController.videoItem['pic'], bvid: videoDetailController.bvid, aid: IdUtils.bv2av(videoDetailController.bvid), - currentCid: videoDetailController.cid.value, + cid: videoDetailController.cid.value, + // count: videoIntroController.videoDetail.value.pages!.length, + // name: videoIntroController.videoDetail.value.pages!, isReversed: videoIntroController.videoDetail.value.isPageReversed, changeFucCall: videoDetailController.videoType == @@ -2127,12 +2132,23 @@ class _VideoDetailPageVState extends State ), Expanded( child: Obx( - () => ListSheetContent( - index: videoDetailController.seasonIndex.value, - season: videoIntroController.videoDetail.value.ugcSeason, + () => EpisodePanel( + heroTag: heroTag, + videoIntroController: videoIntroController, + type: EpisodeType.season, + initialTabIndex: videoDetailController.seasonIndex.value, + cover: videoDetailController.videoItem['pic'], + seasonId: + videoIntroController.videoDetail.value.ugcSeason!.id, + list: videoIntroController + .videoDetail.value.ugcSeason!.sections!, + // count: videoIntroController + // .videoDetail.value.ugcSeason!.epCount!, + // name: + // videoIntroController.videoDetail.value.ugcSeason!.title!, bvid: videoDetailController.bvid, aid: IdUtils.bv2av(videoDetailController.bvid), - currentCid: videoDetailController.seasonCid ?? 0, + cid: videoDetailController.seasonCid ?? 0, isReversed: videoIntroController.videoDetail.value.isSeasonReversed, changeFucCall: videoDetailController.videoType == @@ -2243,14 +2259,22 @@ class _VideoDetailPageVState extends State videoDetailController.showMediaListPanel(context); return; } - Widget listSheetContent([bool? enableSlide]) => ListSheetContent( + Widget listSheetContent([bool? enableSlide]) => EpisodePanel( + heroTag: heroTag, + videoIntroController: videoIntroController, + type: season != null + ? EpisodeType.season + : episodes is List + ? EpisodeType.part + : EpisodeType.bangumi, + cover: videoDetailController.videoItem['pic'], enableSlide: enableSlide, - index: index, - season: season, + initialTabIndex: index ?? 0, bvid: bvid, aid: aid, - currentCid: cid, - episodes: episodes, + cid: cid, + seasonId: season?.id, + list: season != null ? season.sections : [episodes], isReversed: videoDetailController.videoType == SearchType.media_bangumi ? null diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 082c0b7f1..52633893b 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -13,7 +13,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -483,11 +482,7 @@ class _PLVideoPlayerState extends State int currentCid = plPlayerController.cid; String bvid = plPlayerController.bvid; List episodes = []; - if (isPage) { - final List pages = - videoIntroController!.videoDetail.value.pages!; - episodes = pages; - } else if (isSeason) { + if (isSeason) { final List sections = videoIntroController!.videoDetail.value.ugcSeason!.sections!; for (int i = 0; i < sections.length; i++) { @@ -500,6 +495,10 @@ class _PLVideoPlayerState extends State } } } + } else if (isPage) { + final List pages = + videoIntroController!.videoDetail.value.pages!; + episodes = pages; } else if (isBangumi) { episodes = (bangumiIntroController!.loadingState.value as Success) .response @@ -507,8 +506,10 @@ class _PLVideoPlayerState extends State } widget.showEpisodes?.call( index, - isPage ? null : videoIntroController?.videoDetail.value.ugcSeason, - episodes, + isSeason + ? videoIntroController?.videoDetail.value.ugcSeason! + : null, + isSeason ? null : episodes, bvid, IdUtils.bv2av(bvid), currentCid, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index f7b68e759..b8e5b240f 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -63,7 +63,6 @@ class Routes { // 热门 CustomGetPage(name: '/hot', page: () => const HotPage()), // 视频详情 - CustomGetPage(name: '/video', page: () => const VideoDetailPage()), CustomGetPage(name: '/videoV', page: () => const VideoDetailPageV()), // CustomGetPage(name: '/webview', page: () => const WebviewPageNew()),