feat: 字幕展示
This commit is contained in:
@ -483,4 +483,7 @@ class Api {
|
|||||||
|
|
||||||
/// 激活buvid3
|
/// 激活buvid3
|
||||||
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
|
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
|
||||||
|
|
||||||
|
/// 获取字幕配置
|
||||||
|
static const getSubtitleConfig = '/x/player/v2';
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import '../models/model_rec_video_item.dart';
|
|||||||
import '../models/user/fav_folder.dart';
|
import '../models/user/fav_folder.dart';
|
||||||
import '../models/video/ai.dart';
|
import '../models/video/ai.dart';
|
||||||
import '../models/video/play/url.dart';
|
import '../models/video/play/url.dart';
|
||||||
|
import '../models/video/subTitile/result.dart';
|
||||||
import '../models/video_detail_res.dart';
|
import '../models/video_detail_res.dart';
|
||||||
import '../utils/recommend_filter.dart';
|
import '../utils/recommend_filter.dart';
|
||||||
import '../utils/storage.dart';
|
import '../utils/storage.dart';
|
||||||
@ -475,4 +476,19 @@ class VideoHttp {
|
|||||||
return {'status': false, 'data': []};
|
return {'status': false, 'data': []};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future getSubtitle({int? cid, String? bvid}) async {
|
||||||
|
var res = await Request().get(Api.getSubtitleConfig, data: {
|
||||||
|
'cid': cid,
|
||||||
|
'bvid': bvid,
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': SubTitlteModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'data': [], 'msg': res.data['msg']};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
20
lib/models/video/subTitile/content.dart
Normal file
20
lib/models/video/subTitile/content.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
class SubTitileContentModel {
|
||||||
|
double? from;
|
||||||
|
double? to;
|
||||||
|
int? location;
|
||||||
|
String? content;
|
||||||
|
|
||||||
|
SubTitileContentModel({
|
||||||
|
this.from,
|
||||||
|
this.to,
|
||||||
|
this.location,
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
SubTitileContentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
from = json['from'];
|
||||||
|
to = json['to'];
|
||||||
|
location = json['location'];
|
||||||
|
content = json['content'];
|
||||||
|
}
|
||||||
|
}
|
70
lib/models/video/subTitile/result.dart
Normal file
70
lib/models/video/subTitile/result.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
class SubTitlteModel {
|
||||||
|
SubTitlteModel({
|
||||||
|
this.aid,
|
||||||
|
this.bvid,
|
||||||
|
this.cid,
|
||||||
|
this.loginMid,
|
||||||
|
this.loginMidHash,
|
||||||
|
this.isOwner,
|
||||||
|
this.name,
|
||||||
|
this.subtitles,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? aid;
|
||||||
|
String? bvid;
|
||||||
|
int? cid;
|
||||||
|
int? loginMid;
|
||||||
|
String? loginMidHash;
|
||||||
|
bool? isOwner;
|
||||||
|
String? name;
|
||||||
|
List<SubTitlteItemModel>? subtitles;
|
||||||
|
|
||||||
|
factory SubTitlteModel.fromJson(Map<String, dynamic> json) => SubTitlteModel(
|
||||||
|
aid: json["aid"],
|
||||||
|
bvid: json["bvid"],
|
||||||
|
cid: json["cid"],
|
||||||
|
loginMid: json["login_mid"],
|
||||||
|
loginMidHash: json["login_mid_hash"],
|
||||||
|
isOwner: json["is_owner"],
|
||||||
|
name: json["name"],
|
||||||
|
subtitles: json["subtitle"] != null
|
||||||
|
? json["subtitle"]["subtitles"]
|
||||||
|
.map<SubTitlteItemModel>((x) => SubTitlteItemModel.fromJson(x))
|
||||||
|
.toList()
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubTitlteItemModel {
|
||||||
|
SubTitlteItemModel({
|
||||||
|
this.id,
|
||||||
|
this.lan,
|
||||||
|
this.lanDoc,
|
||||||
|
this.isLock,
|
||||||
|
this.subtitleUrl,
|
||||||
|
this.type,
|
||||||
|
this.aiType,
|
||||||
|
this.aiStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? id;
|
||||||
|
String? lan;
|
||||||
|
String? lanDoc;
|
||||||
|
bool? isLock;
|
||||||
|
String? subtitleUrl;
|
||||||
|
int? type;
|
||||||
|
int? aiType;
|
||||||
|
int? aiStatus;
|
||||||
|
|
||||||
|
factory SubTitlteItemModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
SubTitlteItemModel(
|
||||||
|
id: json["id"],
|
||||||
|
lan: json["lan"],
|
||||||
|
lanDoc: json["lan_doc"],
|
||||||
|
isLock: json["is_lock"],
|
||||||
|
subtitleUrl: json["subtitle_url"],
|
||||||
|
type: json["type"],
|
||||||
|
aiType: json["ai_type"],
|
||||||
|
aiStatus: json["ai_status"],
|
||||||
|
);
|
||||||
|
}
|
@ -19,6 +19,8 @@ import 'package:pilipala/utils/utils.dart';
|
|||||||
import 'package:pilipala/utils/video_utils.dart';
|
import 'package:pilipala/utils/video_utils.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
|
import '../../../http/index.dart';
|
||||||
|
import '../../../models/video/subTitile/content.dart';
|
||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
|
|
||||||
@ -91,6 +93,8 @@ class VideoDetailController extends GetxController
|
|||||||
late int cacheAudioQa;
|
late int cacheAudioQa;
|
||||||
|
|
||||||
PersistentBottomSheetController? replyReplyBottomSheetCtr;
|
PersistentBottomSheetController? replyReplyBottomSheetCtr;
|
||||||
|
RxList<SubTitileContentModel> subtitleContents =
|
||||||
|
<SubTitileContentModel>[].obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -139,6 +143,7 @@ class VideoDetailController extends GetxController
|
|||||||
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
|
||||||
defaultValue: AudioQuality.hiRes.code);
|
defaultValue: AudioQuality.hiRes.code);
|
||||||
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
oid.value = IdUtils.bv2av(Get.parameters['bvid']!);
|
||||||
|
getSubtitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
showReplyReplyPanel() {
|
showReplyReplyPanel() {
|
||||||
@ -381,4 +386,35 @@ class VideoDetailController extends GetxController
|
|||||||
? replyReplyBottomSheetCtr!.close()
|
? replyReplyBottomSheetCtr!.close()
|
||||||
: print('replyReplyBottomSheetCtr is null');
|
: print('replyReplyBottomSheetCtr is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字幕配置
|
||||||
|
Future getSubtitle() async {
|
||||||
|
var result = await VideoHttp.getSubtitle(bvid: bvid, cid: cid.value);
|
||||||
|
if (result['status']) {
|
||||||
|
if (result['data'].subtitles.isNotEmpty) {
|
||||||
|
SmartDialog.showToast('字幕加载中...');
|
||||||
|
var subtitle = result['data'].subtitles.first;
|
||||||
|
getSubtitleContent(subtitle.subtitleUrl);
|
||||||
|
}
|
||||||
|
return result['data'];
|
||||||
|
} else {
|
||||||
|
SmartDialog.showToast(result['msg'].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字幕内容
|
||||||
|
Future getSubtitleContent(String url) async {
|
||||||
|
var res = await Request().get('https:$url');
|
||||||
|
subtitleContents.value = res.data['body'].map<SubTitileContentModel>((e) {
|
||||||
|
return SubTitileContentModel.fromJson(e);
|
||||||
|
}).toList();
|
||||||
|
setSubtitleContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubtitleContent() {
|
||||||
|
plPlayerController.subtitleContent.value = '';
|
||||||
|
if (subtitleContents.isNotEmpty) {
|
||||||
|
plPlayerController.subtitleContents = subtitleContents;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
void didPopNext() async {
|
void didPopNext() async {
|
||||||
if (plPlayerController != null &&
|
if (plPlayerController != null &&
|
||||||
plPlayerController!.videoPlayerController != null) {
|
plPlayerController!.videoPlayerController != null) {
|
||||||
setState(() => isShowing = true);
|
setState(() {
|
||||||
|
videoDetailController.setSubtitleContent();
|
||||||
|
isShowing = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
videoDetailController.isFirstTime = false;
|
videoDetailController.isFirstTime = false;
|
||||||
final bool autoplay = autoPlayEnable;
|
final bool autoplay = autoPlayEnable;
|
||||||
|
@ -21,6 +21,7 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
import 'package:status_bar_control/status_bar_control.dart';
|
import 'package:status_bar_control/status_bar_control.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
import '../../models/video/subTitile/content.dart';
|
||||||
// import 'package:wakelock_plus/wakelock_plus.dart';
|
// import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
Box videoStorage = GStrorage.video;
|
Box videoStorage = GStrorage.video;
|
||||||
@ -231,6 +232,10 @@ class PlPlayerController {
|
|||||||
// 播放顺序相关
|
// 播放顺序相关
|
||||||
PlayRepeat playRepeat = PlayRepeat.pause;
|
PlayRepeat playRepeat = PlayRepeat.pause;
|
||||||
|
|
||||||
|
RxList<SubTitileContentModel> subtitleContents =
|
||||||
|
<SubTitileContentModel>[].obs;
|
||||||
|
RxString subtitleContent = ''.obs;
|
||||||
|
|
||||||
void updateSliderPositionSecond() {
|
void updateSliderPositionSecond() {
|
||||||
int newSecond = _sliderPosition.value.inSeconds;
|
int newSecond = _sliderPosition.value.inSeconds;
|
||||||
if (sliderPositionSeconds.value != newSecond) {
|
if (sliderPositionSeconds.value != newSecond) {
|
||||||
@ -277,8 +282,7 @@ class PlPlayerController {
|
|||||||
danmakuDurationVal =
|
danmakuDurationVal =
|
||||||
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
|
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
|
||||||
// 描边粗细
|
// 描边粗细
|
||||||
strokeWidth =
|
strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
|
||||||
localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
|
|
||||||
playRepeat = PlayRepeat.values.toList().firstWhere(
|
playRepeat = PlayRepeat.values.toList().firstWhere(
|
||||||
(e) =>
|
(e) =>
|
||||||
e.value ==
|
e.value ==
|
||||||
@ -566,6 +570,8 @@ class PlPlayerController {
|
|||||||
_sliderPosition.value = event;
|
_sliderPosition.value = event;
|
||||||
updateSliderPositionSecond();
|
updateSliderPositionSecond();
|
||||||
}
|
}
|
||||||
|
querySubtitleContent(
|
||||||
|
videoPlayerController!.state.position.inSeconds.toDouble());
|
||||||
|
|
||||||
/// 触发回调事件
|
/// 触发回调事件
|
||||||
for (var element in _positionListeners) {
|
for (var element in _positionListeners) {
|
||||||
@ -1050,6 +1056,17 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void querySubtitleContent(double progress) {
|
||||||
|
if (subtitleContents.isNotEmpty) {
|
||||||
|
for (var content in subtitleContents) {
|
||||||
|
if (progress >= content.from! && progress <= content.to!) {
|
||||||
|
subtitleContent.value = content.content!;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setPlayRepeat(PlayRepeat type) {
|
setPlayRepeat(PlayRepeat type) {
|
||||||
playRepeat = type;
|
playRepeat = type;
|
||||||
videoStorage.put(VideoBoxKey.playRepeat, type.value);
|
videoStorage.put(VideoBoxKey.playRepeat, type.value);
|
||||||
|
@ -16,7 +16,6 @@ import 'package:pilipala/plugin/pl_player/utils.dart';
|
|||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
|
||||||
import 'models/bottom_progress_behavior.dart';
|
import 'models/bottom_progress_behavior.dart';
|
||||||
import 'widgets/app_bar_ani.dart';
|
import 'widgets/app_bar_ani.dart';
|
||||||
import 'widgets/backward_seek.dart';
|
import 'widgets/backward_seek.dart';
|
||||||
@ -428,6 +427,42 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
if (widget.danmuWidget != null)
|
if (widget.danmuWidget != null)
|
||||||
Positioned.fill(top: 4, child: widget.danmuWidget!),
|
Positioned.fill(top: 4, child: widget.danmuWidget!),
|
||||||
|
|
||||||
|
widget.controller.subscriptions.isNotEmpty
|
||||||
|
? Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 30,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Obx(
|
||||||
|
() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: widget.controller.subtitleContent.value != ''
|
||||||
|
? Colors.black.withOpacity(0.4)
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.controller.subtitleContent.value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: nil,
|
||||||
|
|
||||||
/// 手势
|
/// 手势
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
left: 16,
|
left: 16,
|
||||||
|
Reference in New Issue
Block a user