opt: video subtitle

avoid refetching subtitle
fix stuck when parsing large subtitle body

opt: viewpoints

Update README.md

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2024-12-02 12:54:23 +08:00
parent a0b1e23727
commit cbdd8e77db
15 changed files with 348 additions and 121 deletions

View File

@@ -47,6 +47,10 @@
## feat
- [x] 显示视频分段信息
- [x] 调节字幕大小
- [x] 调节全屏弹幕大小
- [x] 收藏夹/稍后再看多选删除
- [x] 搜索用户动态
- [x] 直播弹幕
- [x] 修改头像/用户名/签名/性别/生日

View File

@@ -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<ListSheetContent> createState() => _ListSheetContentState();
@@ -137,7 +136,7 @@ class _ListSheetContentState extends State<ListSheetContent>
}
}
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<ListSheetContent>
_mediumButton(
tooltip: '关闭',
icon: Icons.close,
onPressed: widget.onClose,
onPressed: Get.back,
),
],
),

View File

@@ -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),
);
}

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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<ExtraSetting> {
GlobalData().grpcReply = value;
},
),
SetSwitchItem(
title: '显示视频分段信息',
leading: Transform.rotate(
angle: pi / 2,
child: Icon(Icons.reorder),
),
setKey: SettingBoxKey.showViewPoints,
defaultVal: true,
),
Obx(
() => ListTile(
enableFeedback: true,

View File

@@ -44,9 +44,7 @@ class _SetSwitchItemState extends State<SetSwitchItem> {
// 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('重启生效');
}

View File

@@ -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<Pair<SegmentType, SkipType>>? _blockSettings;
List<Color>? _blockColor;
RxList<SegmentModel> segmentList = <SegmentModel>[].obs;
List<Segment> viewPointList = <Segment>[];
List<Segment>? _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<PostSegmentModel>? list;
void onBlock(BuildContext context) {
PersistentBottomSheetController? ctr;
list ??= <PostSegmentModel>[];
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<Map<String, String>> _vttSubtitles = <Map<String, String>>[];
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;
}
}
}
}

View File

@@ -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<VideoDetailPage>
}
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<VideoDetailPage>
),
),
showEpisodes: showEpisodes,
showViewPoints: showViewPoints,
),
);
@@ -1058,6 +1064,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
),
showEpisodes: showEpisodes,
showViewPoints: showViewPoints,
),
);
} else {
@@ -1379,8 +1386,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
showEpisodes(index, season, episodes, bvid, aid, cid) {
PersistentBottomSheetController? bottomSheetController;
Widget listSheetContent() => ListSheetContent(
index: index,
season: season,
@@ -1392,15 +1397,162 @@ class _VideoDetailPageState extends State<VideoDetailPage>
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<Icon?>((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),
);
}
}
}

View File

@@ -106,13 +106,13 @@ class _WebviewPageNewState extends State<WebviewPageNew> {
}
},
itemBuilder: (context) => <PopupMenuEntry<WebviewMenuItem>>[
...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),
)),

View File

@@ -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<Map<String, String>> _vttSubtitles = <Map<String, String>>[].obs;
final RxInt _vttSubtitlesIndex = 0.obs;
// 视频字幕
final RxList<Map<String, String>> vttSubtitles = <Map<String, String>>[].obs;
final RxInt vttSubtitlesIndex = 0.obs;
Timer? _timer;
Timer? _timerForSeek;
@@ -115,6 +114,7 @@ class PlPlayerController {
Timer? timerForTrackingMouse;
final RxList<Segment> viewPointList = <Segment>[].obs;
final RxBool showVP = true.obs;
final RxList<Segment> segmentList = <Segment>[].obs;
// final Durations durations;
@@ -164,10 +164,6 @@ class PlPlayerController {
Rx<bool> get mute => _mute;
Stream<bool> get onMuteChanged => _mute.stream;
// 视频字幕
RxList<Map<String, String>> get vttSubtitles => _vttSubtitles;
RxInt get vttSubtitlesIndex => _vttSubtitlesIndex;
/// [videoPlayerController] instance of Player
Player? get videoPlayerController => _videoPlayerController;
@@ -408,6 +404,9 @@ class PlPlayerController {
Future<void> setDataSource(
DataSource dataSource, {
List<Segment>? segmentList,
List<Segment>? viewPointList,
List<Map<String, String>>? 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 ?? <Segment>[];
this.viewPointList.value = viewPointList ?? <Segment>[];
this.vttSubtitles.value = vttSubtitles ?? <Map<String, String>>[];
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<String, String> s = _vttSubtitles[index];
Map<String, String> s = vttSubtitles[index];
_videoPlayerController?.setSubtitleTrack(SubtitleTrack.data(
s['text']!,
title: s['title']!,
language: s['language']!,
));
_vttSubtitlesIndex.value = index;
vttSubtitlesIndex.value = index;
}
static void updatePlayCount() {

View File

@@ -10,4 +10,5 @@ enum BottomControlType {
speed,
fullscreen,
custom,
viewPoints,
}

View File

@@ -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<Widget>? customWidgets;
final Function? showEpisodes;
final VoidCallback? showViewPoints;
@override
State<PLVideoPlayer> createState() => _PLVideoPlayerState();
@@ -236,7 +239,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Map<BottomControlType, Widget> videoProgressWidgets = {
/// 上一集
BottomControlType.pre: Container(
width: 42,
width: 35,
height: 30,
alignment: Alignment.center,
child: ComBtn(
@@ -268,7 +271,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 下一集
BottomControlType.next: Container(
width: 42,
width: 35,
height: 30,
alignment: Alignment.center,
child: ComBtn(
@@ -330,9 +333,32 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 空白占位
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<PLVideoPlayer>
/// 画面比例
BottomControlType.fit: SizedBox(
width: 42,
width: 35,
height: 30,
child: TextButton(
onPressed: () => plPlayerController.toggleVideoFit(),
@@ -408,7 +434,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
() => plPlayerController.vttSubtitles.isEmpty
? const SizedBox.shrink()
: SizedBox(
width: 42,
width: 35,
height: 30,
child: PopupMenuButton<int>(
onSelected: (int value) {
@@ -434,7 +460,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}).toList();
},
child: Container(
width: 42,
width: 35,
height: 30,
alignment: Alignment.center,
child: const Icon(
@@ -450,7 +476,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 播放速度
BottomControlType.speed: SizedBox(
width: 42,
width: 35,
height: 30,
child: PopupMenuButton<double>(
onSelected: (double value) {
@@ -473,7 +499,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}).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<PLVideoPlayer>
/// 全屏
BottomControlType.fullscreen: SizedBox(
width: 42,
width: 35,
height: 30,
child: Obx(() => ComBtn(
icon: Icon(
@@ -510,6 +536,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
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<PLVideoPlayer>
segmentColors: plPlayerController.segmentList,
),
),
if (plPlayerController.viewPointList.isNotEmpty)
if (plPlayerController.viewPointList.isNotEmpty &&
plPlayerController.showVP.value)
CustomPaint(
size: Size(double.infinity, 3.5),
painter: SegmentProgressBar(

View File

@@ -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(

View File

@@ -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<double> 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',