mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-05 18:20:10 +08:00
feat: load file sub
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -828,7 +828,13 @@ class VideoHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future playInfo({String? aid, String? bvid, required int cid}) async {
|
||||
static Future playInfo({
|
||||
String? aid,
|
||||
String? bvid,
|
||||
required int cid,
|
||||
dynamic seasonId,
|
||||
dynamic epId,
|
||||
}) async {
|
||||
assert(aid != null || bvid != null);
|
||||
var res = await Request().get(
|
||||
Api.playInfo,
|
||||
@@ -836,6 +842,8 @@ class VideoHttp {
|
||||
'aid': ?aid,
|
||||
'bvid': ?bvid,
|
||||
'cid': cid,
|
||||
'season_id': ?seasonId,
|
||||
'ep_id': ?epId,
|
||||
}),
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
@@ -848,32 +856,31 @@ class VideoHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static String _subtitleTimecode(num seconds) {
|
||||
int h = seconds ~/ 3600;
|
||||
seconds %= 3600;
|
||||
int m = seconds ~/ 60;
|
||||
seconds %= 60;
|
||||
String sms = seconds.toStringAsFixed(3).padLeft(6, '0');
|
||||
return h == 0
|
||||
? "${m.toString().padLeft(2, '0')}:$sms"
|
||||
: "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:$sms";
|
||||
}
|
||||
|
||||
static String processList(List list) {
|
||||
final sb = StringBuffer('WEBVTT\n\n')
|
||||
..writeAll(
|
||||
list.map(
|
||||
(item) =>
|
||||
'${item?['sid'] ?? 0}\n${_subtitleTimecode(item['from'])} --> ${_subtitleTimecode(item['to'])}\n${item['content'].trim()}',
|
||||
),
|
||||
'\n\n',
|
||||
);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static Future<String?> vttSubtitles(String subtitleUrl) async {
|
||||
String subtitleTimecode(num seconds) {
|
||||
int h = seconds ~/ 3600;
|
||||
seconds %= 3600;
|
||||
int m = seconds ~/ 60;
|
||||
seconds %= 60;
|
||||
String sms = seconds.toStringAsFixed(3).padLeft(6, '0');
|
||||
return h == 0
|
||||
? "${m.toString().padLeft(2, '0')}:$sms"
|
||||
: "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:$sms";
|
||||
}
|
||||
|
||||
String processList(List list) {
|
||||
final sb = StringBuffer('WEBVTT\n\n')
|
||||
..writeAll(
|
||||
list.map(
|
||||
(item) =>
|
||||
'${item?['sid'] ?? 0}\n${subtitleTimecode(item['from'])} --> ${subtitleTimecode(item['to'])}\n${item['content'].trim()}',
|
||||
),
|
||||
'\n\n',
|
||||
);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
var res = await Request().get("https:$subtitleUrl");
|
||||
|
||||
if (res.data?['body'] case List list) {
|
||||
return compute<List, String>(processList, list);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ class Subtitle {
|
||||
String? subtitleUrlV2;
|
||||
bool isAi = false;
|
||||
|
||||
Subtitle({
|
||||
required this.lan,
|
||||
this.lanDoc,
|
||||
});
|
||||
|
||||
Subtitle.fromJson(Map<String, dynamic> json) {
|
||||
lan = json["lan"];
|
||||
isAi = json["type"] == 1;
|
||||
|
||||
@@ -1477,7 +1477,7 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
|
||||
RxList<Subtitle> subtitles = RxList<Subtitle>();
|
||||
late final Map<int, String> _vttSubtitles = {};
|
||||
late final Map<int, String> vttSubtitles = {};
|
||||
late final RxInt vttSubtitlesIndex = (-1).obs;
|
||||
late final RxBool showVP = true.obs;
|
||||
late final RxList<Segment> viewPointList = <Segment>[].obs;
|
||||
@@ -1493,17 +1493,18 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
|
||||
void setSub(String subtitle) {
|
||||
final sub = subtitles[index - 1];
|
||||
plPlayerController.videoPlayerController?.setSubtitleTrack(
|
||||
SubtitleTrack.data(
|
||||
subtitle,
|
||||
title: subtitles[index - 1].lanDoc,
|
||||
language: subtitles[index - 1].lan,
|
||||
title: sub.lanDoc,
|
||||
language: sub.lan,
|
||||
),
|
||||
);
|
||||
vttSubtitlesIndex.value = index;
|
||||
}
|
||||
|
||||
String? subtitle = _vttSubtitles[index - 1];
|
||||
String? subtitle = vttSubtitles[index - 1];
|
||||
if (subtitle != null) {
|
||||
setSub(subtitle);
|
||||
} else {
|
||||
@@ -1511,7 +1512,7 @@ class VideoDetailController extends GetxController
|
||||
subtitles[index - 1].subtitleUrl!,
|
||||
);
|
||||
if (result != null) {
|
||||
_vttSubtitles[index - 1] = result;
|
||||
vttSubtitles[index - 1] = result;
|
||||
setSub(result);
|
||||
}
|
||||
}
|
||||
@@ -1548,12 +1549,17 @@ class VideoDetailController extends GetxController
|
||||
late bool continuePlayingPart = Pref.continuePlayingPart;
|
||||
|
||||
Future<void> _queryPlayInfo() async {
|
||||
_vttSubtitles.clear();
|
||||
vttSubtitles.clear();
|
||||
vttSubtitlesIndex.value = 0;
|
||||
if (plPlayerController.showViewPoints) {
|
||||
viewPointList.clear();
|
||||
}
|
||||
var res = await VideoHttp.playInfo(bvid: bvid, cid: cid.value);
|
||||
var res = await VideoHttp.playInfo(
|
||||
bvid: bvid,
|
||||
cid: cid.value,
|
||||
seasonId: seasonId,
|
||||
epId: epId,
|
||||
);
|
||||
if (res['status']) {
|
||||
PlayInfoData playInfo = res['data'];
|
||||
// interactive video
|
||||
@@ -1699,6 +1705,11 @@ class VideoDetailController extends GetxController
|
||||
// danmaku
|
||||
savedDanmaku = null;
|
||||
|
||||
// subtitle
|
||||
subtitles.clear();
|
||||
vttSubtitlesIndex.value = -1;
|
||||
vttSubtitles.clear();
|
||||
|
||||
if (!isFileSource) {
|
||||
// language
|
||||
languages.value = null;
|
||||
@@ -1709,11 +1720,6 @@ class VideoDetailController extends GetxController
|
||||
dmTrend.value = null;
|
||||
}
|
||||
|
||||
// subtitle
|
||||
subtitles.clear();
|
||||
vttSubtitlesIndex.value = -1;
|
||||
_vttSubtitles.clear();
|
||||
|
||||
// view point
|
||||
if (plPlayerController.showViewPoints) {
|
||||
viewPointList.clear();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
@@ -12,12 +13,14 @@ import 'package:PiliPlus/http/danmaku.dart';
|
||||
import 'package:PiliPlus/http/danmaku_block.dart';
|
||||
import 'package:PiliPlus/http/init.dart';
|
||||
import 'package:PiliPlus/http/live.dart';
|
||||
import 'package:PiliPlus/http/video.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/audio_quality.dart';
|
||||
import 'package:PiliPlus/models/common/video/cdn_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_decode_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
||||
import 'package:PiliPlus/models/video/play/url.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_play_info/subtitle.dart';
|
||||
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
|
||||
import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart';
|
||||
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
|
||||
@@ -43,6 +46,7 @@ import 'package:PiliPlus/utils/utils.dart';
|
||||
import 'package:PiliPlus/utils/video_utils.dart';
|
||||
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -592,17 +596,66 @@ class HeaderControlState extends State<HeaderControl> {
|
||||
leading: const Icon(CustomIcons.dm_settings, size: 20),
|
||||
title: const Text('弹幕设置', style: titleStyle),
|
||||
),
|
||||
if (!videoDetailCtr.isFileSource)
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
showSetSubtitle();
|
||||
},
|
||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||
title: const Text('字幕设置', style: titleStyle),
|
||||
),
|
||||
if (videoDetailCtr.subtitles.isNotEmpty)
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
Get.back();
|
||||
showSetSubtitle();
|
||||
},
|
||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||
title: const Text('字幕设置', style: titleStyle),
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
try {
|
||||
final FilePickerResult? file = await FilePicker.platform
|
||||
.pickFiles();
|
||||
if (file != null) {
|
||||
final first = file.files.first;
|
||||
final path = first.path;
|
||||
if (path != null) {
|
||||
final file = File(path);
|
||||
final stream = file.openRead().transform(
|
||||
utf8.decoder,
|
||||
);
|
||||
final buffer = StringBuffer();
|
||||
await for (final chunk in stream) {
|
||||
if (!mounted) return;
|
||||
buffer.write(chunk);
|
||||
}
|
||||
if (!mounted) return;
|
||||
String sub = buffer.toString();
|
||||
final name = first.name;
|
||||
if (name.endsWith('.json')) {
|
||||
sub = await compute<List, String>(
|
||||
VideoHttp.processList,
|
||||
jsonDecode(sub)['body'],
|
||||
);
|
||||
if (!mounted) return;
|
||||
}
|
||||
final length = videoDetailCtr.subtitles.length;
|
||||
videoDetailCtr
|
||||
..subtitles.add(
|
||||
Subtitle(
|
||||
lan: '',
|
||||
lanDoc: name.split('.').firstOrNull ?? name,
|
||||
),
|
||||
)
|
||||
..vttSubtitles[length] = sub
|
||||
..setSubtitle(length + 1);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
SmartDialog.showToast('加载失败: $e');
|
||||
}
|
||||
},
|
||||
leading: const Icon(Icons.file_open_outlined, size: 20),
|
||||
title: const Text('加载字幕', style: titleStyle),
|
||||
),
|
||||
if (!videoDetailCtr.isFileSource &&
|
||||
videoDetailCtr.subtitles.isNotEmpty)
|
||||
ListTile(
|
||||
dense: true,
|
||||
onTap: () {
|
||||
@@ -1065,9 +1118,11 @@ class HeaderControlState extends State<HeaderControl> {
|
||||
dense: true,
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final url = item.subtitleUrl;
|
||||
if (url == null || url.isEmpty) return;
|
||||
try {
|
||||
final res = await Request.dio.get<Uint8List>(
|
||||
item.subtitleUrl!.http2https,
|
||||
url.http2https,
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
headers: Constants.baseHeaders,
|
||||
|
||||
@@ -666,20 +666,30 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
return [
|
||||
PopupMenuItem<int>(
|
||||
value: 0,
|
||||
height: 35,
|
||||
onTap: () => videoDetailController.setSubtitle(0),
|
||||
child: const Text(
|
||||
"关闭字幕",
|
||||
style: TextStyle(color: Colors.white),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
...videoDetailController.subtitles.indexed.map((e) {
|
||||
return PopupMenuItem<int>(
|
||||
value: e.$1 + 1,
|
||||
height: 35,
|
||||
onTap: () =>
|
||||
videoDetailController.setSubtitle(e.$1 + 1),
|
||||
child: Text(
|
||||
"${e.$2.lanDoc}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -870,10 +880,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
BottomControlType.viewPoints,
|
||||
if (isNotFileSource || anySeason) BottomControlType.episode,
|
||||
if (flag) BottomControlType.fit,
|
||||
if (isNotFileSource) ...[
|
||||
BottomControlType.aiTranslate,
|
||||
BottomControlType.subtitle,
|
||||
],
|
||||
if (isNotFileSource) BottomControlType.aiTranslate,
|
||||
BottomControlType.subtitle,
|
||||
BottomControlType.speed,
|
||||
if (isNotFileSource && flag) BottomControlType.qa,
|
||||
if (!plPlayerController.isDesktopPip) BottomControlType.fullscreen,
|
||||
|
||||
Reference in New Issue
Block a user