mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
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:
@@ -47,6 +47,10 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 显示视频分段信息
|
||||
- [x] 调节字幕大小
|
||||
- [x] 调节全屏弹幕大小
|
||||
- [x] 收藏夹/稍后再看多选删除
|
||||
- [x] 搜索用户动态
|
||||
- [x] 直播弹幕
|
||||
- [x] 修改头像/用户名/签名/性别/生日
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('重启生效');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -10,4 +10,5 @@ enum BottomControlType {
|
||||
speed,
|
||||
fullscreen,
|
||||
custom,
|
||||
viewPoints,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user