import 'dart:io'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/services/shutdown_timer_service.dart'; import '../../../../models/common/search_type.dart'; import '../../../../models/video_detail_res.dart'; import '../introduction/index.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { const HeaderControl({ this.controller, this.videoDetailCtr, this.floating, this.bvid, this.videoType, super.key, }); final PlPlayerController? controller; final VideoDetailController? videoDetailCtr; final Floating? floating; final String? bvid; final SearchType? videoType; @override State createState() => _HeaderControlState(); @override Size get preferredSize => throw UnimplementedError(); } class _HeaderControlState extends State { late PlayUrlModel videoInfo; static const TextStyle subTitleStyle = TextStyle(fontSize: 12); static const TextStyle titleStyle = TextStyle(fontSize: 14); Size get preferredSize => const Size(double.infinity, kToolbarHeight); final Box localCache = GStrorage.localCache; final Box videoStorage = GStrorage.video; late List speedsList; double buttonSpace = 8; bool showTitle = false; late String heroTag; late VideoIntroController videoIntroController; late VideoDetailData videoDetail; @override void initState() { super.initState(); videoInfo = widget.videoDetailCtr!.data; speedsList = widget.controller!.speedsList; fullScreenStatusListener(); heroTag = Get.arguments['heroTag']; videoIntroController = Get.put(VideoIntroController(bvid: widget.bvid!), tag: heroTag); } void fullScreenStatusListener() { widget.videoDetailCtr!.plPlayerController.isFullScreen .listen((bool isFullScreen) { if (isFullScreen) { showTitle = true; } else { showTitle = false; } /// TODO setState() called after dispose() if (mounted) { setState(() {}); } }); } /// 设置面板 void showSettingSheet() { showModalBottomSheet( elevation: 0, context: context, backgroundColor: Colors.transparent, builder: (_) { return Container( width: double.infinity, height: 460, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), child: Column( children: [ SizedBox( height: 35, child: Center( child: Container( width: 32, height: 3, decoration: BoxDecoration( color: Theme.of(context) .colorScheme .onSecondaryContainer .withOpacity(0.5), borderRadius: const BorderRadius.all(Radius.circular(3))), ), ), ), Expanded( child: Material( child: ListView( children: [ // ListTile( // onTap: () {}, // dense: true, // enabled: false, // leading: // const Icon(Icons.network_cell_outlined, size: 20), // title: Text('省流模式', style: titleStyle), // subtitle: Text('低画质 | 减少视频缓存', style: subTitleStyle), // trailing: Transform.scale( // scale: 0.75, // child: Switch( // thumbIcon: MaterialStateProperty.resolveWith( // (Set states) { // if (states.isNotEmpty && // states.first == MaterialState.selected) { // return const Icon(Icons.done); // } // return null; // All other states will use the default thumbIcon. // }), // value: false, // onChanged: (value) => {}, // ), // ), // ), ListTile( onTap: () async { final res = await UserHttp.toViewLater( bvid: widget.videoDetailCtr!.bvid); SmartDialog.showToast(res['msg']); Get.back(); }, dense: true, leading: const Icon(Icons.watch_later_outlined, size: 20), title: const Text('添加至「稍后再看」', style: titleStyle), ), ListTile( onTap: () => {Get.back(), scheduleExit()}, dense: true, leading: const Icon(Icons.hourglass_top_outlined, size: 20), title: const Text('定时关闭(测试)', style: titleStyle), ), ListTile( onTap: () => {Get.back(), showSetVideoQa()}, dense: true, leading: const Icon(Icons.play_circle_outline, size: 20), title: const Text('选择画质', style: titleStyle), subtitle: Text( '当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}', style: subTitleStyle), ), if (widget.videoDetailCtr!.currentAudioQa != null) ListTile( onTap: () => {Get.back(), showSetAudioQa()}, dense: true, leading: const Icon(Icons.album_outlined, size: 20), title: const Text('选择音质', style: titleStyle), subtitle: Text( '当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}', style: subTitleStyle), ), ListTile( onTap: () => {Get.back(), showSetDecodeFormats()}, dense: true, leading: const Icon(Icons.av_timer_outlined, size: 20), title: const Text('解码格式', style: titleStyle), subtitle: Text( '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', style: subTitleStyle), ), ListTile( onTap: () => {Get.back(), showSetRepeat()}, dense: true, leading: const Icon(Icons.repeat, size: 20), title: const Text('播放顺序', style: titleStyle), subtitle: Text(widget.controller!.playRepeat.description, style: subTitleStyle), ), ListTile( onTap: () => {Get.back(), showSetDanmaku()}, dense: true, leading: const Icon(Icons.subtitles_outlined, size: 20), title: const Text('弹幕设置', style: titleStyle), ), ], ), )) ], ), ); }, clipBehavior: Clip.hardEdge, isScrollControlled: true, ); } /// 定时关闭 void scheduleExit() async { const List scheduleTimeChoices = [ -1, 15, 30, 60, ]; showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: double.infinity, height: 500, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), padding: const EdgeInsets.only(left: 14, right: 14), child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 30), const Center(child: Text('定时关闭', style: titleStyle)), const SizedBox(height: 10), for (final int choice in scheduleTimeChoices) ...[ ListTile( onTap: () { shutdownTimerService.scheduledExitInMinutes = choice; shutdownTimerService.startShutdownTimer(); Get.back(); }, contentPadding: const EdgeInsets.only(), dense: true, title: Text(choice == -1 ? "禁用" : "$choice分钟后"), trailing: shutdownTimerService .scheduledExitInMinutes == choice ? Icon( Icons.done, color: Theme.of(context).colorScheme.primary, ) : const SizedBox(), ) ], const SizedBox(height: 6), const Center( child: SizedBox( width: 100, child: Divider(height: 1), )), const SizedBox(height: 10), ListTile( onTap: () { shutdownTimerService.waitForPlayingCompleted = !shutdownTimerService.waitForPlayingCompleted; setState(() {}); }, dense: true, contentPadding: const EdgeInsets.only(), title: const Text("额外等待视频播放完毕", style: titleStyle), trailing: Switch( // thumb color (round icon) activeColor: Theme.of(context).colorScheme.primary, activeTrackColor: Theme.of(context).colorScheme.primaryContainer, inactiveThumbColor: Theme.of(context).colorScheme.primaryContainer, inactiveTrackColor: Theme.of(context).colorScheme.background, splashRadius: 10.0, // boolean variable value value: shutdownTimerService.waitForPlayingCompleted, // changes the state of the switch onChanged: (value) => setState(() => shutdownTimerService.waitForPlayingCompleted = value), ), ), const SizedBox(height: 10), Row( children: [ const Text('倒计时结束:', style: titleStyle), const Spacer(), ActionRowLineItem( onTap: () { shutdownTimerService.exitApp = false; setState(() {}); // Get.back(); }, text: " 暂停视频 ", selectStatus: !shutdownTimerService.exitApp, ), const Spacer(), // const SizedBox(width: 10), ActionRowLineItem( onTap: () { shutdownTimerService.exitApp = true; setState(() {}); // Get.back(); }, text: " 退出APP ", selectStatus: shutdownTimerService.exitApp, ) ], ), ]), ), ), ); }); }, ); } /// 选择倍速 void showSetSpeedSheet() { final double currentSpeed = widget.controller!.playbackSpeed; showDialog( context: Get.context!, builder: (BuildContext context) { return AlertDialog( title: const Text('播放速度'), content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Wrap( spacing: 8, runSpacing: 2, children: [ for (final double i in speedsList) ...[ if (i == currentSpeed) ...[ FilledButton( onPressed: () async { // setState(() => currentSpeed = i), await widget.controller!.setPlaybackSpeed(i); Get.back(); }, child: Text(i.toString()), ), ] else ...[ FilledButton.tonal( onPressed: () async { // setState(() => currentSpeed = i), await widget.controller!.setPlaybackSpeed(i); Get.back(); }, child: Text(i.toString()), ), ] ] ], ); }), actions: [ TextButton( onPressed: () => Get.back(), child: Text( '取消', style: TextStyle(color: Theme.of(context).colorScheme.outline), ), ), TextButton( onPressed: () async { await widget.controller!.setDefaultSpeed(); Get.back(); }, child: const Text('默认速度'), ), ], ); }, ); } /// 选择画质 void showSetVideoQa() { final List videoFormat = videoInfo.supportFormats!; final VideoQuality currentVideoQa = widget.videoDetailCtr!.currentVideoQa; /// 总质量分类 final int totalQaSam = videoFormat.length; /// 可用的质量分类 int userfulQaSam = 0; final List video = videoInfo.dash!.video!; final Set idSet = {}; for (final VideoItem item in video) { final int id = item.id!; if (!idSet.contains(id)) { idSet.add(id); userfulQaSam++; } } showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return Container( width: double.infinity, height: 310, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), child: Column( children: [ SizedBox( height: 45, child: GestureDetector( onTap: () { SmartDialog.showToast('标灰画质可能需要bilibili会员'); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('选择画质', style: titleStyle), SizedBox(width: buttonSpace), Icon( Icons.info_outline, size: 16, color: Theme.of(context).colorScheme.outline, ) ], ), ), ), Expanded( child: Material( child: Scrollbar( child: ListView( children: [ for (int i = 0; i < totalQaSam; i++) ...[ ListTile( onTap: () { if (currentVideoQa.code == videoFormat[i].quality) { return; } final int quality = videoFormat[i].quality!; widget.videoDetailCtr!.currentVideoQa = VideoQualityCode.fromCode(quality)!; widget.videoDetailCtr!.updatePlayer(); Get.back(); }, dense: true, // 可能包含会员解锁画质 enabled: i >= totalQaSam - userfulQaSam, contentPadding: const EdgeInsets.only(left: 20, right: 20), title: Text(videoFormat[i].newDesc!), subtitle: Text( videoFormat[i].format!, style: subTitleStyle, ), trailing: currentVideoQa.code == videoFormat[i].quality ? Icon( Icons.done, color: Theme.of(context).colorScheme.primary, ) : const SizedBox(), ), ] ], ), ), ), ), ], ), ); }, ); } /// 选择音质 void showSetAudioQa() { final AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa!; final List audio = videoInfo.dash!.audio!; showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return Container( width: double.infinity, height: 250, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), child: Column( children: [ const SizedBox( height: 45, child: Center(child: Text('选择音质', style: titleStyle))), Expanded( child: Material( child: ListView( children: [ for (final AudioItem i in audio) ...[ ListTile( onTap: () { if (currentAudioQa.code == i.id) { return; } final int quality = i.id!; widget.videoDetailCtr!.currentAudioQa = AudioQualityCode.fromCode(quality)!; widget.videoDetailCtr!.updatePlayer(); Get.back(); }, dense: true, contentPadding: const EdgeInsets.only(left: 20, right: 20), title: Text(i.quality!), subtitle: Text( i.codecs!, style: subTitleStyle, ), trailing: currentAudioQa.code == i.id ? Icon( Icons.done, color: Theme.of(context).colorScheme.primary, ) : const SizedBox(), ), ] ], ), ), ), ], ), ); }, ); } // 选择解码格式 void showSetDecodeFormats() { // 当前选中的解码格式 final VideoDecodeFormats currentDecodeFormats = widget.videoDetailCtr!.currentDecodeFormats; final VideoItem firstVideo = widget.videoDetailCtr!.firstVideo; // 当前视频可用的解码格式 final List videoFormat = videoInfo.supportFormats!; final List list = videoFormat .firstWhere((FormatItem e) => e.quality == firstVideo.quality!.code) .codecs!; showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return Container( width: double.infinity, height: 250, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), child: Column( children: [ SizedBox( height: 45, child: Center(child: Text('选择解码格式', style: titleStyle))), Expanded( child: Material( child: ListView( children: [ for (var i in list) ...[ ListTile( onTap: () { if (i.startsWith(currentDecodeFormats.code)) return; widget.videoDetailCtr!.currentDecodeFormats = VideoDecodeFormatsCode.fromString(i)!; widget.videoDetailCtr!.updatePlayer(); Get.back(); }, dense: true, contentPadding: const EdgeInsets.only(left: 20, right: 20), title: Text(VideoDecodeFormatsCode.fromString(i)! .description!), subtitle: Text( i!, style: subTitleStyle, ), trailing: i.startsWith(currentDecodeFormats.code) ? Icon( Icons.done, color: Theme.of(context).colorScheme.primary, ) : const SizedBox(), ), ] ], ), ), ), ], ), ); }, ); } /// 弹幕功能 void showSetDanmaku() async { // 屏蔽类型 final List> blockTypesList = [ {'value': 5, 'label': '顶部'}, {'value': 2, 'label': '滚动'}, {'value': 4, 'label': '底部'}, {'value': 6, 'label': '彩色'}, ]; final List blockTypes = widget.controller!.blockTypes; // 显示区域 final List> showAreas = [ {'value': 0.25, 'label': '1/4屏'}, {'value': 0.5, 'label': '半屏'}, {'value': 0.75, 'label': '3/4屏'}, {'value': 1.0, 'label': '满屏'}, ]; double showArea = widget.controller!.showArea; // 不透明度 double opacityVal = widget.controller!.opacityVal; // 字体大小 double fontSizeVal = widget.controller!.fontSizeVal; // 弹幕速度 double danmakuDurationVal = widget.controller!.danmakuDurationVal; // 弹幕描边 double strokeWidth = widget.controller!.strokeWidth; final DanmakuController danmakuController = widget.controller!.danmakuController!; await showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Container( width: double.infinity, height: 580, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), padding: const EdgeInsets.only(left: 14, right: 14), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 45, child: Center(child: Text('弹幕设置', style: titleStyle)), ), const SizedBox(height: 10), const Text('按类型屏蔽'), Padding( padding: const EdgeInsets.only(top: 12, bottom: 18), child: Row( children: [ for (final Map i in blockTypesList) ...[ ActionRowLineItem( onTap: () async { final bool isChoose = blockTypes.contains(i['value']); if (isChoose) { blockTypes.remove(i['value']); } else { blockTypes.add(i['value']); } widget.controller!.blockTypes = blockTypes; setState(() {}); try { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = currentOption.copyWith( hideTop: blockTypes.contains(5), hideBottom: blockTypes.contains(4), hideScroll: blockTypes.contains(2), // 添加或修改其他需要修改的选项属性 ); danmakuController.updateOption(updatedOption); } catch (_) {} }, text: i['label'], selectStatus: blockTypes.contains(i['value']), ), const SizedBox(width: 10), ] ], ), ), const Text('显示区域'), Padding( padding: const EdgeInsets.only(top: 12, bottom: 18), child: Row( children: [ for (final Map i in showAreas) ...[ ActionRowLineItem( onTap: () { showArea = i['value']; widget.controller!.showArea = showArea; setState(() {}); try { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = currentOption.copyWith(area: i['value']); danmakuController.updateOption(updatedOption); } catch (_) {} }, text: i['label'], selectStatus: showArea == i['value'], ), const SizedBox(width: 10), ] ], ), ), Text('不透明度 ${opacityVal * 100}%'), Padding( padding: const EdgeInsets.only( top: 0, bottom: 6, left: 10, right: 10, ), child: SliderTheme( data: SliderThemeData( trackShape: MSliderTrackShape(), thumbColor: Theme.of(context).colorScheme.primary, activeTrackColor: Theme.of(context).colorScheme.primary, trackHeight: 10, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6.0), ), child: Slider( min: 0, max: 1, value: opacityVal, divisions: 10, label: '${opacityVal * 100}%', onChanged: (double val) { opacityVal = val; widget.controller!.opacityVal = opacityVal; setState(() {}); try { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = currentOption.copyWith(opacity: val); danmakuController.updateOption(updatedOption); } catch (_) {} }, ), ), ), Text('描边粗细 $strokeWidth'), Padding( padding: const EdgeInsets.only( top: 0, bottom: 6, left: 10, right: 10, ), child: SliderTheme( data: SliderThemeData( trackShape: MSliderTrackShape(), thumbColor: Theme.of(context).colorScheme.primary, activeTrackColor: Theme.of(context).colorScheme.primary, trackHeight: 10, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6.0), ), child: Slider( min: 0, max: 3, value: strokeWidth, divisions: 6, label: '$strokeWidth', onChanged: (double val) { strokeWidth = val; widget.controller!.strokeWidth = val; setState(() {}); try { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = currentOption.copyWith(strokeWidth: val); danmakuController.updateOption(updatedOption); } catch (_) {} }, ), ), ), Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'), Padding( padding: const EdgeInsets.only( top: 0, bottom: 6, left: 10, right: 10, ), child: SliderTheme( data: SliderThemeData( trackShape: MSliderTrackShape(), thumbColor: Theme.of(context).colorScheme.primary, activeTrackColor: Theme.of(context).colorScheme.primary, trackHeight: 10, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6.0), ), child: Slider( min: 0.5, max: 2.5, value: fontSizeVal, divisions: 20, label: '${(fontSizeVal * 100).toStringAsFixed(1)}%', onChanged: (double val) { fontSizeVal = val; widget.controller!.fontSizeVal = fontSizeVal; setState(() {}); try { final DanmakuOption currentOption = danmakuController.option; final DanmakuOption updatedOption = currentOption.copyWith( fontSize: (15 * fontSizeVal).toDouble(), ); danmakuController.updateOption(updatedOption); } catch (_) {} }, ), ), ), Text('弹幕时长 ${danmakuDurationVal.toString()} 秒'), Padding( padding: const EdgeInsets.only( top: 0, bottom: 6, left: 10, right: 10, ), child: SliderTheme( data: SliderThemeData( trackShape: MSliderTrackShape(), thumbColor: Theme.of(context).colorScheme.primary, activeTrackColor: Theme.of(context).colorScheme.primary, trackHeight: 10, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6.0), ), child: Slider( min: 2, max: 16, value: danmakuDurationVal, divisions: 28, label: danmakuDurationVal.toString(), onChanged: (double val) { danmakuDurationVal = val; widget.controller!.danmakuDurationVal = danmakuDurationVal; setState(() {}); try { final DanmakuOption updatedOption = danmakuController.option.copyWith( duration: val / widget.controller!.playbackSpeed); danmakuController.updateOption(updatedOption); } catch (_) {} }, ), ), ), ], ), ), ); }); }, ); } /// 播放顺序 void showSetRepeat() async { showModalBottomSheet( context: context, elevation: 0, backgroundColor: Colors.transparent, builder: (BuildContext context) { return Container( width: double.infinity, height: 250, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), child: Column( children: [ SizedBox( height: 45, child: Center(child: Text('选择播放顺序', style: titleStyle))), Expanded( child: Material( child: ListView( children: [ for (final PlayRepeat i in PlayRepeat.values) ...[ ListTile( onTap: () { widget.controller!.setPlayRepeat(i); Get.back(); }, dense: true, contentPadding: const EdgeInsets.only(left: 20, right: 20), title: Text(i.description), trailing: widget.controller!.playRepeat == i ? Icon( Icons.done, color: Theme.of(context).colorScheme.primary, ) : const SizedBox(), ) ], ], ), ), ), ], ), ); }, ); } @override Widget build(BuildContext context) { final _ = widget.controller!; const TextStyle textStyle = TextStyle( color: Colors.white, fontSize: 12, ); final bool isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; return AppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, elevation: 0, scrolledUnderElevation: 0, primary: false, centerTitle: false, automaticallyImplyLeading: false, titleSpacing: 14, title: Row( children: [ ComBtn( icon: const Icon( FontAwesomeIcons.arrowLeft, size: 15, color: Colors.white, ), fuc: () => >{ if (widget.controller!.isFullScreen.value) {widget.controller!.triggerFullScreen(status: false)} else { if (MediaQuery.of(context).orientation == Orientation.landscape) { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]) }, Get.back() } }, ), SizedBox(width: buttonSpace), if (showTitle && isLandscape && widget.videoType == SearchType.video) ...[ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200), child: Text( videoIntroController.videoDetail.value.title ?? '', style: const TextStyle( color: Colors.white, fontSize: 16, ), ), ), if (videoIntroController.isShowOnlineTotal) Text( '${videoIntroController.total.value}人正在看', style: const TextStyle( color: Colors.white, fontSize: 12, ), ) ], ) ] else ...[ ComBtn( icon: const Icon( FontAwesomeIcons.house, size: 15, color: Colors.white, ), fuc: () async { // 销毁播放器实例 await widget.controller!.dispose(type: 'all'); if (mounted) { Navigator.popUntil( context, (Route route) => route.isFirst); } }, ), ], const Spacer(), // ComBtn( // icon: const Icon( // FontAwesomeIcons.cropSimple, // size: 15, // color: Colors.white, // ), // fuc: () => _.screenshot(), // ), SizedBox(width: buttonSpace), if (Platform.isAndroid) ...[ SizedBox( width: 34, height: 34, child: IconButton( style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), ), onPressed: () async { bool canUsePiP = false; widget.controller!.hiddenControls(false); try { canUsePiP = await widget.floating!.isPipAvailable; } on PlatformException catch (_) { canUsePiP = false; } if (canUsePiP) { final Rational aspectRatio = Rational( widget.videoDetailCtr!.data.dash!.video!.first.width!, widget.videoDetailCtr!.data.dash!.video!.first.height!, ); await widget.floating!.enable(aspectRatio: aspectRatio); } else {} }, icon: const Icon( Icons.picture_in_picture_outlined, size: 19, color: Colors.white, ), ), ), SizedBox(width: buttonSpace), ], Obx( () => SizedBox( width: 45, height: 34, child: TextButton( style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), ), onPressed: () => showSetSpeedSheet(), child: Text( '${_.playbackSpeed}X', style: textStyle, ), ), ), ), SizedBox(width: buttonSpace), ComBtn( icon: const Icon( Icons.more_vert_outlined, size: 18, color: Colors.white, ), fuc: () => showSettingSheet(), ), ], ), ); } } class MSliderTrackShape extends RoundedRectSliderTrackShape { @override Rect getPreferredRect({ required RenderBox parentBox, Offset offset = Offset.zero, SliderThemeData? sliderTheme, bool isEnabled = false, bool isDiscrete = false, }) { const double trackHeight = 3; final double trackLeft = offset.dx; final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2 + 4; final double trackWidth = parentBox.size.width; return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); } }