diff --git a/lib/http/api.dart b/lib/http/api.dart index 2e758439..559c6fdb 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -483,4 +483,7 @@ class Api { /// 激活buvid3 static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; + + /// 获取字幕配置 + static const getSubtitleConfig = '/x/player/v2'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index 30df62c3..a852e74b 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -8,6 +8,7 @@ import '../models/model_rec_video_item.dart'; import '../models/user/fav_folder.dart'; import '../models/video/ai.dart'; import '../models/video/play/url.dart'; +import '../models/video/subTitile/result.dart'; import '../models/video_detail_res.dart'; import '../utils/recommend_filter.dart'; import '../utils/storage.dart'; @@ -475,4 +476,19 @@ class VideoHttp { 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']}; + } + } } diff --git a/lib/models/video/subTitile/content.dart b/lib/models/video/subTitile/content.dart new file mode 100644 index 00000000..b18098a4 --- /dev/null +++ b/lib/models/video/subTitile/content.dart @@ -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 json) { + from = json['from']; + to = json['to']; + location = json['location']; + content = json['content']; + } +} diff --git a/lib/models/video/subTitile/result.dart b/lib/models/video/subTitile/result.dart new file mode 100644 index 00000000..389378fa --- /dev/null +++ b/lib/models/video/subTitile/result.dart @@ -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? subtitles; + + factory SubTitlteModel.fromJson(Map 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((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 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"], + ); +} diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 7465c6f2..198da362 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -19,6 +19,8 @@ import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/video_utils.dart'; import 'package:screen_brightness/screen_brightness.dart'; +import '../../../http/index.dart'; +import '../../../models/video/subTitile/content.dart'; import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; @@ -91,6 +93,8 @@ class VideoDetailController extends GetxController late int cacheAudioQa; PersistentBottomSheetController? replyReplyBottomSheetCtr; + RxList subtitleContents = + [].obs; @override void onInit() { @@ -139,6 +143,7 @@ class VideoDetailController extends GetxController cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultValue: AudioQuality.hiRes.code); oid.value = IdUtils.bv2av(Get.parameters['bvid']!); + getSubtitle(); } showReplyReplyPanel() { @@ -381,4 +386,35 @@ class VideoDetailController extends GetxController ? replyReplyBottomSheetCtr!.close() : 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((e) { + return SubTitileContentModel.fromJson(e); + }).toList(); + setSubtitleContent(); + } + + setSubtitleContent() { + plPlayerController.subtitleContent.value = ''; + if (subtitleContents.isNotEmpty) { + plPlayerController.subtitleContents = subtitleContents; + } + } } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index febca105..449c377e 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -236,7 +236,10 @@ class _VideoDetailPageState extends State void didPopNext() async { if (plPlayerController != null && plPlayerController!.videoPlayerController != null) { - setState(() => isShowing = true); + setState(() { + videoDetailController.setSubtitleContent(); + isShowing = true; + }); } videoDetailController.isFirstTime = false; final bool autoplay = autoPlayEnable; diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index dfa580ab..081b9ad9 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -21,6 +21,7 @@ import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; import 'package:status_bar_control/status_bar_control.dart'; import 'package:universal_platform/universal_platform.dart'; +import '../../models/video/subTitile/content.dart'; // import 'package:wakelock_plus/wakelock_plus.dart'; Box videoStorage = GStrorage.video; @@ -231,6 +232,10 @@ class PlPlayerController { // 播放顺序相关 PlayRepeat playRepeat = PlayRepeat.pause; + RxList subtitleContents = + [].obs; + RxString subtitleContent = ''.obs; + void updateSliderPositionSecond() { int newSecond = _sliderPosition.value.inSeconds; if (sliderPositionSeconds.value != newSecond) { @@ -277,8 +282,7 @@ class PlPlayerController { danmakuDurationVal = localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); // 描边粗细 - strokeWidth = - localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); + strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); playRepeat = PlayRepeat.values.toList().firstWhere( (e) => e.value == @@ -566,6 +570,8 @@ class PlPlayerController { _sliderPosition.value = event; updateSliderPositionSecond(); } + querySubtitleContent( + videoPlayerController!.state.position.inSeconds.toDouble()); /// 触发回调事件 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) { playRepeat = type; videoStorage.put(VideoBoxKey.playRepeat, type.value); diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index d073945b..e7a8ce73 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -16,7 +16,6 @@ import 'package:pilipala/plugin/pl_player/utils.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:screen_brightness/screen_brightness.dart'; - import 'models/bottom_progress_behavior.dart'; import 'widgets/app_bar_ani.dart'; import 'widgets/backward_seek.dart'; @@ -428,6 +427,42 @@ class _PLVideoPlayerState extends State if (widget.danmuWidget != null) 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( left: 16,