diff --git a/README.md b/README.md index 37a2e36fa..f2efbf10a 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ ## feat +- [x] AI 原声翻译 - [x] SuperChat - [x] 播放课堂视频 - [x] 发起投票 diff --git a/lib/http/video.dart b/lib/http/video.dart index 022e374e0..0d9f91c44 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -196,6 +196,7 @@ class VideoHttp { dynamic seasonId, required bool tryLook, required VideoType videoType, + String? language, }) async { final params = await WbiSign.makSign({ 'avid': ?avid, @@ -214,6 +215,7 @@ class VideoHttp { 'web_location': 1315873, // 免登录查看1080p if (tryLook) 'try_look': 1, + 'cur_language': ?language, }); try { diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index 4d19b3d3b..b891fa314 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -39,6 +39,8 @@ class PlayUrlModel { List? supportFormats; int? lastPlayTime; int? lastPlayCid; + String? curLanguage; + Language? language; PlayUrlModel.fromJson(Map json) { from = json['from']; @@ -62,6 +64,51 @@ class PlayUrlModel { .toList(); lastPlayTime = json['last_play_time']; lastPlayCid = json['last_play_cid']; + curLanguage = json['cur_language']; + language = json['language'] == null + ? null + : Language.fromJson(json['language']); + } +} + +class Language { + Language({ + this.support, + this.items, + }); + + bool? support; + List? items; + + Language.fromJson(Map json) { + support = json['support']; + items = (json['items'] as List?) + ?.map((e) => LanguageItem.fromJson(e)) + .toList(); + } +} + +class LanguageItem { + LanguageItem({ + this.lang, + this.title, + this.subtitleLang, + this.videoDetext, + this.videoMouthShapeChange, + }); + + String? lang; + String? title; + String? subtitleLang; + bool? videoDetext; + bool? videoMouthShapeChange; + + LanguageItem.fromJson(Map json) { + lang = json['lang']; + title = json['title']; + subtitleLang = json['subtitle_lang']; + videoDetext = json['video_detext']; + videoMouthShapeChange = json['video_mouth_shape_change']; } } diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index bd4cda145..e4d37e505 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -29,6 +29,7 @@ class BottomControl extends StatelessWidget { @override Widget build(BuildContext context) { + final isFullScreen = plPlayerController.isFullScreen.value; return AppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, @@ -40,6 +41,7 @@ class BottomControl extends StatelessWidget { PlayOrPauseButton(plPlayerController: plPlayerController), ComBtn( height: 30, + tooltip: '刷新', icon: const Icon( Icons.refresh, size: 18, @@ -50,6 +52,7 @@ class BottomControl extends StatelessWidget { const Spacer(), ComBtn( height: 30, + tooltip: '屏蔽', icon: const Icon( size: 18, Icons.block, @@ -74,6 +77,7 @@ class BottomControl extends StatelessWidget { final enableShowLiveDanmaku = plPlayerController.enableShowLiveDanmaku.value; return ComBtn( + tooltip: "${enableShowLiveDanmaku ? '关闭' : '开启'}弹幕", icon: enableShowLiveDanmaku ? const Icon( size: 18, @@ -100,6 +104,7 @@ class BottomControl extends StatelessWidget { ), Obx( () => PopupMenuButton( + tooltip: '画面比例', initialValue: plPlayerController.videoFit.value, color: Colors.black.withValues(alpha: 0.8), itemBuilder: (context) { @@ -132,6 +137,7 @@ class BottomControl extends StatelessWidget { ), Obx( () => PopupMenuButton( + tooltip: '画质', padding: EdgeInsets.zero, initialValue: liveRoomCtr.currentQn, color: Colors.black.withValues(alpha: 0.8), @@ -165,22 +171,20 @@ class BottomControl extends StatelessWidget { ), ComBtn( height: 30, - icon: plPlayerController.isFullScreen.value + tooltip: isFullScreen ? '退出全屏' : '全屏', + icon: isFullScreen ? const Icon( Icons.fullscreen_exit, - semanticLabel: '退出全屏', size: 24, color: Colors.white, ) : const Icon( Icons.fullscreen, - semanticLabel: '全屏', size: 24, color: Colors.white, ), - onTap: () => plPlayerController.triggerFullScreen( - status: !plPlayerController.isFullScreen.value, - ), + onTap: () => + plPlayerController.triggerFullScreen(status: !isFullScreen), ), ], ), diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index cadd4f115..77c64b112 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -72,11 +72,13 @@ class LiveHeaderControl extends StatelessWidget { children: [ if (isFullScreen) ComBtn( + tooltip: '返回', icon: const Icon(FontAwesomeIcons.arrowLeft, size: 15), onTap: () => plPlayerController.triggerFullScreen(status: false), ), child, ComBtn( + tooltip: '发弹幕', icon: const Icon( size: 18, Icons.comment_outlined, @@ -88,6 +90,7 @@ class LiveHeaderControl extends StatelessWidget { () { final onlyPlayAudio = plPlayerController.onlyPlayAudio.value; return ComBtn( + tooltip: '仅播放音频', onTap: () { plPlayerController.onlyPlayAudio.value = !onlyPlayAudio; onPlayAudio(); @@ -108,6 +111,7 @@ class LiveHeaderControl extends StatelessWidget { ), if (Platform.isAndroid) ComBtn( + tooltip: '画中画', onTap: () async { try { var floating = Floating(); @@ -130,6 +134,7 @@ class LiveHeaderControl extends StatelessWidget { ), ), ComBtn( + tooltip: '定时关闭', onTap: () => PageUtils.scheduleExit( context, plPlayerController.isFullScreen.value, diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 5f6ce953d..6dfb80725 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -247,13 +247,15 @@ class VideoDetailController extends GetxController imageStatus = false; } + final isLoginVideo = Accounts.get(AccountType.video).isLogin; + @override void onInit() { super.onInit(); args = Get.arguments; videoType = args['videoType']; if (videoType == VideoType.pgc) { - if (!Accounts.get(AccountType.video).isLogin) { + if (!isLoginVideo) { _actualVideoType = VideoType.ugc; } } else if (args['pgcApi'] == true) { @@ -1112,6 +1114,17 @@ class VideoDetailController extends GetxController bool isQuerying = false; + final Rx?> languages = Rx?>(null); + final Rx currLang = Rx(null); + void setLanguage(String language) { + if (!isLoginVideo) { + SmartDialog.showToast('账号未登录'); + return; + } + currLang.value = language; + queryVideoUrl(defaultST: playedTime); + } + // 视频链接 Future queryVideoUrl({ Duration? defaultST, @@ -1142,11 +1155,15 @@ class VideoDetailController extends GetxController seasonId: seasonId, tryLook: plPlayerController.tryLook, videoType: _actualVideoType ?? videoType, + language: currLang.value, ); if (result.isSuccess) { data = result.data; + languages.value = data.language?.items; + currLang.value = data.curLanguage; + if (data.acceptDesc?.contains('试看') == true) { SmartDialog.showToast( '该视频为专属视频,仅提供试看', @@ -1564,6 +1581,10 @@ class VideoDetailController extends GetxController videoUrl = null; audioUrl = null; + // language + languages.value = null; + currLang.value = null; + if (scrollRatio.value != 0) { scrollRatio.refresh(); } diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart index 4cd848902..be14c8420 100644 --- a/lib/plugin/pl_player/models/bottom_control_type.dart +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -12,4 +12,5 @@ enum BottomControlType { superResolution, dmChart, qa, + aiTranslate, } diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index b137b1e0a..c2e7e21b1 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -256,9 +256,9 @@ class _PLVideoPlayerState extends State BottomControlType.pre => ComBtn( width: widgetWidth, height: 30, + tooltip: '上一集', icon: const Icon( Icons.skip_previous, - semanticLabel: '上一集', size: 22, color: Colors.white, ), @@ -273,9 +273,9 @@ class _PLVideoPlayerState extends State BottomControlType.next => ComBtn( width: widgetWidth, height: 30, + tooltip: '下一集', icon: const Icon( Icons.skip_next, - semanticLabel: '下一集', size: 22, color: Colors.white, ), @@ -329,6 +329,7 @@ class _PLVideoPlayerState extends State return ComBtn( width: widgetWidth, height: 30, + tooltip: '高能进度条', icon: videoDetailController.showDmTreandChart.value ? const Icon( Icons.show_chart, @@ -399,11 +400,11 @@ class _PLVideoPlayerState extends State : ComBtn( width: widgetWidth, height: 30, + tooltip: '分段信息', icon: Transform.rotate( angle: math.pi / 2, child: const Icon( MdiIcons.viewHeadline, - semanticLabel: '分段信息', size: 22, color: Colors.white, ), @@ -421,9 +422,9 @@ class _PLVideoPlayerState extends State BottomControlType.episode => ComBtn( width: widgetWidth, height: 30, + tooltip: '选集', icon: const Icon( Icons.list, - semanticLabel: '选集', size: 22, color: Colors.white, ), @@ -501,12 +502,58 @@ class _PLVideoPlayerState extends State ), ), + BottomControlType.aiTranslate => Obx( + () { + final list = videoDetailController.languages.value; + if (list != null && list.isNotEmpty) { + return PopupMenuButton( + tooltip: '原声翻译', + requestFocus: false, + initialValue: videoDetailController.currLang.value, + color: Colors.black.withValues(alpha: 0.8), + itemBuilder: (context) { + return [ + PopupMenuItem( + value: '', + onTap: () => videoDetailController.setLanguage(''), + child: const Text( + "关闭翻译", + style: TextStyle(color: Colors.white), + ), + ), + ...list.map((e) { + return PopupMenuItem( + value: e.lang, + onTap: () => videoDetailController.setLanguage(e.lang!), + child: Text( + e.title!, + style: const TextStyle(color: Colors.white), + ), + ); + }), + ]; + }, + child: SizedBox( + width: widgetWidth, + height: 30, + child: const Icon( + Icons.translate, + size: 18, + color: Colors.white, + ), + ), + ); + } + return const SizedBox.shrink(); + }, + ), + /// 字幕 BottomControlType.subtitle => Obx( () => videoDetailController.subtitles.isEmpty == true ? const SizedBox.shrink() : PopupMenuButton( - tooltip: '选择字幕', + tooltip: '字幕', requestFocus: false, initialValue: videoDetailController.vttSubtitlesIndex.value .clamp( @@ -676,16 +723,15 @@ class _PLVideoPlayerState extends State BottomControlType.fullscreen => ComBtn( width: widgetWidth, height: 30, + tooltip: isFullScreen ? '退出全屏' : '全屏', icon: isFullScreen ? const Icon( Icons.fullscreen_exit, - semanticLabel: '退出全屏', size: 24, color: Colors.white, ) : const Icon( Icons.fullscreen, - semanticLabel: '全屏', size: 24, color: Colors.white, ), @@ -709,6 +755,7 @@ class _PLVideoPlayerState extends State if (plPlayerController.showViewPoints) BottomControlType.viewPoints, if (anySeason) BottomControlType.episode, if (isFullScreen) BottomControlType.fit, + BottomControlType.aiTranslate, BottomControlType.subtitle, BottomControlType.speed, if (isFullScreen) BottomControlType.qa, @@ -1251,6 +1298,8 @@ class _PLVideoPlayerState extends State // 头部、底部控制条 Positioned.fill( + top: -1, + bottom: -1, child: ClipRect( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1489,16 +1538,15 @@ class _PLVideoPlayerState extends State final controlsLock = plPlayerController.controlsLock.value; return ComBtn( + tooltip: controlsLock ? '解锁' : '锁定', icon: controlsLock ? const Icon( FontAwesomeIcons.lock, - semanticLabel: '解锁', size: 15, color: Colors.white, ) : const Icon( FontAwesomeIcons.lockOpen, - semanticLabel: '锁定', size: 15, color: Colors.white, ), @@ -1530,9 +1578,9 @@ class _PLVideoPlayerState extends State borderRadius: BorderRadius.all(Radius.circular(8)), ), child: ComBtn( + tooltip: '截图', icon: const Icon( Icons.photo_camera, - semanticLabel: '截图', size: 20, color: Colors.white, ), diff --git a/lib/plugin/pl_player/widgets/app_bar_ani.dart b/lib/plugin/pl_player/widgets/app_bar_ani.dart index 799471882..b393e4549 100644 --- a/lib/plugin/pl_player/widgets/app_bar_ani.dart +++ b/lib/plugin/pl_player/widgets/app_bar_ani.dart @@ -36,7 +36,7 @@ class AppBarAni extends StatelessWidget { end: Alignment.topCenter, colors: [ Colors.transparent, - Colors.black54, + Color(0xBF000000), ], tileMode: TileMode.mirror, ) @@ -45,7 +45,7 @@ class AppBarAni extends StatelessWidget { end: Alignment.bottomCenter, colors: [ Colors.transparent, - Colors.black54, + Color(0xBF000000), ], tileMode: TileMode.mirror, ), diff --git a/lib/plugin/pl_player/widgets/common_btn.dart b/lib/plugin/pl_player/widgets/common_btn.dart index f40bf6a3a..bda6bc283 100644 --- a/lib/plugin/pl_player/widgets/common_btn.dart +++ b/lib/plugin/pl_player/widgets/common_btn.dart @@ -6,6 +6,7 @@ class ComBtn extends StatelessWidget { final VoidCallback? onLongPress; final double width; final double height; + final String? tooltip; const ComBtn({ super.key, @@ -14,11 +15,12 @@ class ComBtn extends StatelessWidget { this.onLongPress, this.width = 34, this.height = 34, + this.tooltip, }); @override Widget build(BuildContext context) { - return SizedBox( + final child = SizedBox( width: width, height: height, child: GestureDetector( @@ -28,5 +30,9 @@ class ComBtn extends StatelessWidget { child: icon, ), ); + if (tooltip != null) { + return Tooltip(message: tooltip, child: child); + } + return child; } }