diff --git a/README.md b/README.md index a3c693eaf..3f529b2b6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ ## feat +- [x] 显示视频分段信息 +- [x] 调节字幕大小 +- [x] 调节全屏弹幕大小 +- [x] 收藏夹/稍后再看多选删除 - [x] 搜索用户动态 - [x] 直播弹幕 - [x] 修改头像/用户名/签名/性别/生日 diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index ea39f5867..0fa7752c2 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -7,6 +7,7 @@ import 'package:PiliPalaX/models/bangumi/info.dart' as bangumi; import 'package:PiliPalaX/models/video_detail_res.dart' as video; 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'; @@ -23,7 +24,6 @@ class ListSheetContent extends StatefulWidget { this.aid, required this.currentCid, required this.changeFucCall, - required this.onClose, }); final dynamic index; @@ -33,7 +33,6 @@ class ListSheetContent extends StatefulWidget { final int? aid; final int currentCid; final Function changeFucCall; - final VoidCallback? onClose; @override State createState() => _ListSheetContentState(); @@ -137,7 +136,7 @@ class _ListSheetContentState extends State } } SmartDialog.showToast('切换到:$title'); - widget.onClose?.call(); + Get.back(); widget.changeFucCall( episode is bangumi.EpisodeItem ? episode.epId : null, episode.runtimeType.toString() == "EpisodeItem" @@ -327,7 +326,7 @@ class _ListSheetContentState extends State _mediumButton( tooltip: '关闭', icon: Icons.close, - onPressed: widget.onClose, + onPressed: Get.back, ), ], ), diff --git a/lib/common/widgets/segment_progress_bar.dart b/lib/common/widgets/segment_progress_bar.dart index 164f182c2..0d10586d6 100644 --- a/lib/common/widgets/segment_progress_bar.dart +++ b/lib/common/widgets/segment_progress_bar.dart @@ -5,8 +5,19 @@ class Segment { final double end; final Color color; final String? title; + final String? url; + final int? from; + final int? to; - Segment(this.start, this.end, this.color, [this.title]); + Segment( + this.start, + this.end, + this.color, [ + this.title, + this.url, + this.from, + this.to, + ]); } class SegmentProgressBar extends CustomPainter { @@ -76,7 +87,7 @@ class SegmentProgressBar extends CustomPainter { size.width, 0, ), - Paint()..color = Colors.grey[600]!, + Paint()..color = Colors.grey[600]!.withOpacity(0.45), ); } diff --git a/lib/http/video.dart b/lib/http/video.dart index 28097302e..89760af1a 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -4,8 +4,7 @@ import 'package:PiliPalaX/grpc/app/card/v1/card.pb.dart' as card; import 'package:PiliPalaX/grpc/grpc_repo.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import '../common/constants.dart'; import '../models/common/reply_type.dart'; @@ -914,6 +913,12 @@ class VideoHttp { return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}.${ms.toString().padLeft(3, '0')}"; } + String processList(List list) { + return list.fold('WEBVTT\n\n', (previous, item) { + return '$previous${item?['sid'] ?? 0}\n${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n${item['content'].trim()}\n\n'; + }); + } + for (var i in subtitlesJson) { var res = await Request().get("https://${i['subtitle_url'].split('//')[1]}"); @@ -948,21 +953,16 @@ class VideoHttp { ] } */ - if (res.data != null) { - String vttData = "WEBVTT\n\n"; - for (var item in res.data['body']) { - vttData += "${item['sid'] ?? 0}\n"; - vttData += - "${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n"; - vttData += "${item['content'].trim()}\n\n"; - } + if (res.data != null && res.data?['body'] is List) { + String vttData = await compute(processList, res.data['body'] as List); subtitlesVtt.add({ 'language': i['lan'], 'title': i['lan_doc'], 'text': vttData, }); } else { - SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}"); + // SmartDialog.showToast("字幕${i['lan_doc']}加载失败, ${res.data['message']}"); + debugPrint('字幕${i['lan_doc']}加载失败, ${res.data['message']}'); } } if (subtitlesVtt.isNotEmpty) { diff --git a/lib/main.dart b/lib/main.dart index 1c4e00959..9e7893999 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -207,7 +207,7 @@ class MyApp extends StatelessWidget { titleSpacing: 0, centerTitle: false, scrolledUnderElevation: 0, - backgroundColor: Platform.isIOS ? colorScheme.surface : null, + backgroundColor: isDynamic ? null : colorScheme.surface, titleTextStyle: TextStyle(fontSize: 16, color: colorScheme.onSurface), ), navigationBarTheme: NavigationBarThemeData( diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index e886af728..c18d81128 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:PiliPalaX/pages/main/controller.dart'; import 'package:PiliPalaX/pages/member/new/controller.dart' show MemberTabType, MemberTabTypeExt; @@ -212,6 +214,15 @@ class _ExtraSettingState extends State { GlobalData().grpcReply = value; }, ), + SetSwitchItem( + title: '显示视频分段信息', + leading: Transform.rotate( + angle: pi / 2, + child: Icon(Icons.reorder), + ), + setKey: SettingBoxKey.showViewPoints, + defaultVal: true, + ), Obx( () => ListTile( enableFeedback: true, diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart index d7b6ae6c8..d0e144400 100644 --- a/lib/pages/setting/widgets/switch_item.dart +++ b/lib/pages/setting/widgets/switch_item.dart @@ -44,9 +44,7 @@ class _SetSwitchItemState extends State { // if (widget.setKey == SettingBoxKey.autoUpdate && value == true) { // Utils.checkUpdate(); // } - if (widget.onChanged != null) { - widget.onChanged!.call(val); - } + widget.onChanged?.call(val); if (widget.needReboot != null && widget.needReboot!) { SmartDialog.showToast('重启生效'); } diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index d8156343d..0c18ae30a 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -8,6 +8,7 @@ import 'package:PiliPalaX/common/widgets/pair.dart'; import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:PiliPalaX/http/danmaku.dart'; import 'package:PiliPalaX/http/init.dart'; +import 'package:PiliPalaX/models/video/play/subtitle.dart'; import 'package:PiliPalaX/utils/extension.dart'; import 'package:dio/dio.dart'; import 'package:floating/floating.dart'; @@ -284,6 +285,7 @@ class VideoDetailController extends GetxController List>? _blockSettings; List? _blockColor; RxList segmentList = [].obs; + List viewPointList = []; List? _segmentProgressList; Color _getColor(SegmentType segment) => _blockColor?[segment.index] ?? segment.color; @@ -844,6 +846,9 @@ class VideoDetailController extends GetxController }, ), segmentList: _segmentProgressList, + viewPointList: viewPointList, + vttSubtitles: _vttSubtitles, + vttSubtitlesIndex: vttSubtitlesIndex, // 硬解 enableHA: enableHA.value, hwdec: hwdec.value, @@ -877,6 +882,7 @@ class VideoDetailController extends GetxController if (enableSponsorBlock) { await _querySponsorBlock(); } + _getSubtitle(); if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) { SmartDialog.showToast( '该视频为专属视频,仅提供试看', @@ -1020,7 +1026,6 @@ class VideoDetailController extends GetxController List? list; void onBlock(BuildContext context) { - PersistentBottomSheetController? ctr; list ??= []; if (list!.isEmpty) { list!.add( @@ -1034,18 +1039,18 @@ class VideoDetailController extends GetxController ), ); } - ctr = plPlayerController.isFullScreen.value + plPlayerController.isFullScreen.value ? scaffoldKey.currentState?.showBottomSheet( enableDrag: false, - (context) => _postPanel(ctr?.close, false), + (context) => _postPanel(false), ) : childKey.currentState?.showBottomSheet( enableDrag: false, - (context) => _postPanel(ctr?.close), + (context) => _postPanel(), ); } - Widget _postPanel(onClose, [bool isChild = true]) => StatefulBuilder( + Widget _postPanel([bool isChild = true]) => StatefulBuilder( builder: (context, setState) { void updateSegment({ required bool isFirst, @@ -1197,7 +1202,7 @@ class VideoDetailController extends GetxController iconButton( context: context, tooltip: '关闭', - onPressed: onClose, + onPressed: Get.back, icon: Icons.close, ), const SizedBox(width: 16), @@ -1572,4 +1577,73 @@ class VideoDetailController extends GetxController SegmentType.exclusive_access => [ActionType.full], }; } + + List> _vttSubtitles = >[]; + int vttSubtitlesIndex = 0; + + void _getSubtitle() { + _querySubtitles().then((value) { + if (_vttSubtitles.isNotEmpty) { + String preference = setting.get( + SettingBoxKey.subtitlePreference, + defaultValue: SubtitlePreference.values.first.code, + ); + if (preference == 'on') { + vttSubtitlesIndex = 1; + } else if (preference == 'withoutAi') { + for (int i = 1; i < _vttSubtitles.length; i++) { + if (_vttSubtitles[i]['language']!.startsWith('ai')) { + continue; + } + vttSubtitlesIndex = i; + break; + } + } + if (plPlayerController.vttSubtitles.isEmpty) { + plPlayerController.vttSubtitles.value = _vttSubtitles; + plPlayerController.vttSubtitlesIndex.value = vttSubtitlesIndex; + if (vttSubtitlesIndex != 0) { + plPlayerController.setSubtitle(vttSubtitlesIndex); + } + } + } + }); + } + + Future _querySubtitles() async { + Map res = await VideoHttp.subtitlesJson(bvid: bvid, cid: cid.value); + // if (!res["status"]) { + // SmartDialog.showToast('查询字幕错误,${res["msg"]}'); + // } + + if (res["data"] is List && res["data"].isNotEmpty) { + var result = await VideoHttp.vttSubtitles(res["data"]); + if (result != null) { + _vttSubtitles = result; + } + // if (_vttSubtitles.isEmpty) { + // SmartDialog.showToast('字幕均加载失败'); + // } + } + if (GStorage.showViewPoints && + res["view_points"] is List && + res["view_points"].isNotEmpty) { + viewPointList = (res["view_points"] as List).map((item) { + double start = + (item['to'] / ((data.timeLength ?? 0) / 1000)).clamp(0.0, 1.0); + return Segment( + start, + start, + Colors.black87, + item?['content'], + item?['imgUrl'], + item?['from'], + item?['to'], + ); + }).toList(); + if (plPlayerController.viewPointList.isEmpty) { + plPlayerController.viewPointList.value = viewPointList; + } + } + } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 198574ca5..5ac5f10d7 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'dart:math'; import 'package:PiliPalaX/common/constants.dart'; +import 'package:PiliPalaX/common/widgets/icon_button.dart'; import 'package:PiliPalaX/common/widgets/list_sheet.dart'; +import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/models/bangumi/info.dart'; import 'package:PiliPalaX/models/common/reply_type.dart'; @@ -16,6 +18,7 @@ import 'package:PiliPalaX/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPalaX/utils/extension.dart'; import 'package:PiliPalaX/utils/global_data.dart'; import 'package:PiliPalaX/utils/id_utils.dart'; +import 'package:PiliPalaX/utils/utils.dart'; import 'package:auto_orientation/auto_orientation.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:floating/floating.dart'; @@ -350,6 +353,8 @@ class _VideoDetailPageState extends State } if (plPlayerController != null) { _makeHeartBeat(); + videoDetailController.vttSubtitlesIndex = + plPlayerController!.vttSubtitlesIndex.value; videoDetailController.defaultST = plPlayerController!.position.value; plPlayerController!.removeStatusLister(playerListener); plPlayerController!.pause(); @@ -960,6 +965,7 @@ class _VideoDetailPageState extends State ), ), showEpisodes: showEpisodes, + showViewPoints: showViewPoints, ), ); @@ -1058,6 +1064,7 @@ class _VideoDetailPageState extends State ), ), showEpisodes: showEpisodes, + showViewPoints: showViewPoints, ), ); } else { @@ -1379,8 +1386,6 @@ class _VideoDetailPageState extends State } showEpisodes(index, season, episodes, bvid, aid, cid) { - PersistentBottomSheetController? bottomSheetController; - Widget listSheetContent() => ListSheetContent( index: index, season: season, @@ -1392,15 +1397,162 @@ class _VideoDetailPageState extends State videoDetailController.videoType == SearchType.media_bangumi ? bangumiIntroController.changeSeasonOrbangu : videoIntroController.changeSeasonOrbangu, - onClose: bottomSheetController?.close, ); + if (isFullScreen) { + videoDetailController.scaffoldKey.currentState?.showBottomSheet( + (context) => listSheetContent(), + ); + } else { + videoDetailController.childKey.currentState?.showBottomSheet( + (context) => listSheetContent(), + ); + } + } - bottomSheetController = isFullScreen - ? videoDetailController.scaffoldKey.currentState?.showBottomSheet( - (context) => listSheetContent(), - ) - : videoDetailController.scaffoldKey.currentState?.showBottomSheet( - (context) => listSheetContent(), - ); + void showViewPoints() { + Widget listSheetContent(context, [bool isFS = false]) { + int currentIndex = -1; + return StatefulBuilder( + builder: (context, setState) => SizedBox( + height: isFS ? Utils.getSheetHeight(context) : null, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 16, + title: Text('分段信息'), + actions: [ + Text( + '分段进度条', + style: TextStyle(fontSize: 16), + ), + Obx( + () => Transform.scale( + alignment: Alignment.centerLeft, + scale: 0.8, + child: Switch( + thumbIcon: + WidgetStateProperty.resolveWith((states) { + if (states.isNotEmpty && + states.first == WidgetState.selected) { + return const Icon(Icons.done); + } + return null; + }), + value: + videoDetailController.plPlayerController.showVP.value, + onChanged: (value) { + videoDetailController.plPlayerController.showVP.value = + value; + }, + ), + ), + ), + iconButton( + context: context, + size: 30, + icon: Icons.clear, + tooltip: '关闭', + onPressed: Get.back, + ), + const SizedBox(width: 16), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + ...List.generate(videoDetailController.viewPointList.length, + (index) { + Segment segment = + videoDetailController.viewPointList[index]; + if (currentIndex == -1 && + segment.from != null && + segment.to != null) { + if (videoDetailController + .plPlayerController.positionSeconds.value >= + segment.from! && + videoDetailController + .plPlayerController.positionSeconds.value < + segment.to!) { + currentIndex = index; + } + } + return ListTile( + dense: true, + onTap: segment.from != null + ? () { + currentIndex = index; + plPlayerController?.danmakuController?.clear(); + plPlayerController?.videoPlayerController + ?.seek(Duration(seconds: segment.from!)); + setState(() {}); + } + : null, + leading: segment.url?.isNotEmpty == true + ? Container( + margin: const EdgeInsets.symmetric(vertical: 6), + decoration: currentIndex == index + ? BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + width: 1.8, + strokeAlign: + BorderSide.strokeAlignOutside, + color: Theme.of(context) + .colorScheme + .primary, + ), + ) + : null, + child: LayoutBuilder( + builder: (_, constraints) => NetworkImgLayer( + radius: 6, + src: segment.url, + width: constraints.maxHeight * + StyleString.aspectRatio, + height: constraints.maxHeight, + ), + ), + ) + : null, + title: Text( + segment.title ?? '', + style: TextStyle( + fontSize: 14, + fontWeight: + currentIndex == index ? FontWeight.bold : null, + color: currentIndex == index + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + subtitle: Text( + '${segment.from != null ? Utils.timeFormat(segment.from) : ''} - ${segment.to != null ? Utils.timeFormat(segment.to) : ''}', + style: TextStyle( + fontSize: 13, + color: currentIndex == index + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.outline, + ), + ), + ); + }), + SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), + ], + ), + ), + ), + ), + ); + } + + if (isFullScreen) { + videoDetailController.scaffoldKey.currentState?.showBottomSheet( + (context) => listSheetContent(context, true), + ); + } else { + videoDetailController.childKey.currentState?.showBottomSheet( + (context) => listSheetContent(context), + ); + } } } diff --git a/lib/pages/webview/webview_page.dart b/lib/pages/webview/webview_page.dart index a27ac7f27..adb6e1cee 100644 --- a/lib/pages/webview/webview_page.dart +++ b/lib/pages/webview/webview_page.dart @@ -106,13 +106,13 @@ class _WebviewPageNewState extends State { } }, itemBuilder: (context) => >[ - ...WebviewMenuItem.values.sublist(0, 4).map( - (item) => PopupMenuItem(value: item, child: Text(item.name))), + ...WebviewMenuItem.values.sublist(0, 4).map((item) => + PopupMenuItem(value: item, child: Text(item.title))), const PopupMenuDivider(), PopupMenuItem( value: WebviewMenuItem.goBack, child: Text( - WebviewMenuItem.goBack.name, + WebviewMenuItem.goBack.title, style: TextStyle(color: Theme.of(context).colorScheme.error), )), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 91333c3b1..c8c11af3f 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -25,8 +25,6 @@ import 'package:PiliPalaX/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; import 'package:universal_platform/universal_platform.dart'; -import '../../models/video/play/subtitle.dart'; - Box videoStorage = GStorage.video; Box setting = GStorage.setting; Box localCache = GStorage.localCache; @@ -104,8 +102,9 @@ class PlPlayerController { bool _enableHeart = true; late DataSource dataSource; - final RxList> _vttSubtitles = >[].obs; - final RxInt _vttSubtitlesIndex = 0.obs; + // 视频字幕 + final RxList> vttSubtitles = >[].obs; + final RxInt vttSubtitlesIndex = 0.obs; Timer? _timer; Timer? _timerForSeek; @@ -115,6 +114,7 @@ class PlPlayerController { Timer? timerForTrackingMouse; final RxList viewPointList = [].obs; + final RxBool showVP = true.obs; final RxList segmentList = [].obs; // final Durations durations; @@ -164,10 +164,6 @@ class PlPlayerController { Rx get mute => _mute; Stream get onMuteChanged => _mute.stream; - // 视频字幕 - RxList> get vttSubtitles => _vttSubtitles; - RxInt get vttSubtitlesIndex => _vttSubtitlesIndex; - /// [videoPlayerController] instance of Player Player? get videoPlayerController => _videoPlayerController; @@ -408,6 +404,9 @@ class PlPlayerController { Future setDataSource( DataSource dataSource, { List? segmentList, + List? viewPointList, + List>? vttSubtitles, + int? vttSubtitlesIndex, bool autoplay = true, // 默认不循环 PlaylistMode looping = PlaylistMode.none, @@ -431,8 +430,10 @@ class PlPlayerController { }) async { try { this.dataSource = dataSource; - viewPointList.clear(); this.segmentList.value = segmentList ?? []; + this.viewPointList.value = viewPointList ?? []; + this.vttSubtitles.value = vttSubtitles ?? >[]; + this.vttSubtitlesIndex.value = vttSubtitlesIndex ?? 0; _autoPlay = autoplay; _looping = looping; // 初始化视频倍速 @@ -467,35 +468,7 @@ class PlPlayerController { startListeners(); } await _initializePlayer(seekTo: seekTo); - if (videoType.value != 'live' && _cid != 0) { - refreshSubtitles().then((value) { - if (_vttSubtitles.isNotEmpty) { - if (_vttSubtitlesIndex > 0 && - _vttSubtitlesIndex < _vttSubtitles.length) { - setSubtitle(_vttSubtitlesIndex.value); - } else { - String preference = setting.get(SettingBoxKey.subtitlePreference, - defaultValue: SubtitlePreference.values.first.code); - if (preference == 'on') { - setSubtitle(1); - } else if (preference == 'withoutAi') { - bool found = false; - for (int i = 1; i < _vttSubtitles.length; i++) { - if (_vttSubtitles[i]['language']!.startsWith('ai')) { - continue; - } - found = true; - setSubtitle(i); - break; - } - if (!found) _vttSubtitlesIndex.value = 0; - } else { - _vttSubtitlesIndex.value = 0; - } - } - } - }); - } + setSubtitle(this.vttSubtitlesIndex.value); } catch (err, stackTrace) { dataStatus.status.value = DataStatus.error; debugPrint(stackTrace.toString()); @@ -1353,49 +1326,20 @@ class PlPlayerController { } } - Future refreshSubtitles() async { - _vttSubtitles.clear(); - Map res = await VideoHttp.subtitlesJson(bvid: _bvid, cid: _cid); - // if (!res["status"]) { - // SmartDialog.showToast('查询字幕错误,${res["msg"]}'); - // } - - if (res["data"] is List && res["data"].isNotEmpty) { - var result = await VideoHttp.vttSubtitles(res["data"]); - if (result != null) { - _vttSubtitles.value = result; - } - // if (_vttSubtitles.isEmpty) { - // SmartDialog.showToast('字幕均加载失败'); - // } - } - if (res["view_points"] is List && res["view_points"].isNotEmpty) { - viewPointList.value = (res["view_points"] as List).map((item) { - double start = (item['to'] / durationSeconds.value).clamp(0.0, 1.0); - return Segment( - start, - start, - Colors.black, - item['content'], - ); - }).toList(); - } - } - // 设定字幕轨道 setSubtitle(int index) { if (index == 0) { _videoPlayerController?.setSubtitleTrack(SubtitleTrack.no()); - _vttSubtitlesIndex.value = 0; + vttSubtitlesIndex.value = 0; return; } - Map s = _vttSubtitles[index]; + Map s = vttSubtitles[index]; _videoPlayerController?.setSubtitleTrack(SubtitleTrack.data( s['text']!, title: s['title']!, language: s['language']!, )); - _vttSubtitlesIndex.value = index; + vttSubtitlesIndex.value = index; } static void updatePlayCount() { diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart index 0b39be1f9..0829976de 100644 --- a/lib/plugin/pl_player/models/bottom_control_type.dart +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -10,4 +10,5 @@ enum BottomControlType { speed, fullscreen, custom, + viewPoints, } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 7be053964..33b066089 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:PiliPalaX/common/widgets/segment_progress_bar.dart'; import 'package:PiliPalaX/http/loading_state.dart'; @@ -47,6 +48,7 @@ class PLVideoPlayer extends StatefulWidget { this.customWidget, this.customWidgets, this.showEpisodes, + this.showViewPoints, super.key, }); @@ -62,6 +64,7 @@ class PLVideoPlayer extends StatefulWidget { final Widget? customWidget; final List? customWidgets; final Function? showEpisodes; + final VoidCallback? showViewPoints; @override State createState() => _PLVideoPlayerState(); @@ -236,7 +239,7 @@ class _PLVideoPlayerState extends State Map videoProgressWidgets = { /// 上一集 BottomControlType.pre: Container( - width: 42, + width: 35, height: 30, alignment: Alignment.center, child: ComBtn( @@ -268,7 +271,7 @@ class _PLVideoPlayerState extends State /// 下一集 BottomControlType.next: Container( - width: 42, + width: 35, height: 30, alignment: Alignment.center, child: ComBtn( @@ -330,9 +333,32 @@ class _PLVideoPlayerState extends State /// 空白占位 BottomControlType.space: const Spacer(), + /// 分段信息 + BottomControlType.viewPoints: Obx( + () => plPlayerController.viewPointList.isEmpty + ? const SizedBox.shrink() + : Container( + width: 35, + height: 30, + alignment: Alignment.center, + child: ComBtn( + icon: Transform.rotate( + angle: pi / 2, + child: const Icon( + Icons.reorder, + semanticLabel: '分段信息', + size: 22, + color: Colors.white, + ), + ), + fuc: widget.showViewPoints, + ), + ), + ), + /// 选集 BottomControlType.episode: Container( - width: 42, + width: 35, height: 30, alignment: Alignment.center, child: ComBtn( @@ -387,7 +413,7 @@ class _PLVideoPlayerState extends State /// 画面比例 BottomControlType.fit: SizedBox( - width: 42, + width: 35, height: 30, child: TextButton( onPressed: () => plPlayerController.toggleVideoFit(), @@ -408,7 +434,7 @@ class _PLVideoPlayerState extends State () => plPlayerController.vttSubtitles.isEmpty ? const SizedBox.shrink() : SizedBox( - width: 42, + width: 35, height: 30, child: PopupMenuButton( onSelected: (int value) { @@ -434,7 +460,7 @@ class _PLVideoPlayerState extends State }).toList(); }, child: Container( - width: 42, + width: 35, height: 30, alignment: Alignment.center, child: const Icon( @@ -450,7 +476,7 @@ class _PLVideoPlayerState extends State /// 播放速度 BottomControlType.speed: SizedBox( - width: 42, + width: 35, height: 30, child: PopupMenuButton( onSelected: (double value) { @@ -473,7 +499,7 @@ class _PLVideoPlayerState extends State }).toList(); }, child: Container( - width: 42, + width: 35, height: 30, alignment: Alignment.center, child: Obx(() => Text("${plPlayerController.playbackSpeed}X", @@ -485,7 +511,7 @@ class _PLVideoPlayerState extends State /// 全屏 BottomControlType.fullscreen: SizedBox( - width: 42, + width: 35, height: 30, child: Obx(() => ComBtn( icon: Icon( @@ -510,6 +536,7 @@ class _PLVideoPlayerState extends State if (anySeason) BottomControlType.pre, if (anySeason) BottomControlType.next, BottomControlType.space, + BottomControlType.viewPoints, if (anySeason) BottomControlType.episode, if (plPlayerController.isFullScreen.value) BottomControlType.fit, BottomControlType.subtitle, @@ -1114,7 +1141,8 @@ class _PLVideoPlayerState extends State segmentColors: plPlayerController.segmentList, ), ), - if (plPlayerController.viewPointList.isNotEmpty) + if (plPlayerController.viewPointList.isNotEmpty && + plPlayerController.showVP.value) CustomPaint( size: Size(double.infinity, 3.5), painter: SegmentProgressBar( diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index 75e21327a..60e11eab1 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -104,7 +104,8 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { segmentColors: controller!.segmentList, ), ), - if (controller?.viewPointList.isNotEmpty == true) + if (controller?.viewPointList.isNotEmpty == true && + controller?.showVP.value == true) CustomPaint( size: Size(double.infinity, 3.5), painter: SegmentProgressBar( diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 35c7bb3a3..35eb769bc 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -108,6 +108,9 @@ class GStorage { static bool get grpcReply => setting.get(SettingBoxKey.grpcReply, defaultValue: true); + static bool get showViewPoints => + setting.get(SettingBoxKey.showViewPoints, defaultValue: true); + static List get dynamicDetailRatio => setting.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]); @@ -289,6 +292,7 @@ class SettingBoxKey { dynamicPeriod = 'dynamicPeriod', schemeVariant = 'schemeVariant', grpcReply = 'grpcReply', + showViewPoints = 'showViewPoints', // Sponsor Block enableSponsorBlock = 'enableSponsorBlock',