diff --git a/assets/images/video/danmu_close.svg b/assets/images/video/danmu_close.svg new file mode 100644 index 000000000..9f48027b0 --- /dev/null +++ b/assets/images/video/danmu_close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/video/danmu_open.svg b/assets/images/video/danmu_open.svg new file mode 100644 index 000000000..24e8d7a99 --- /dev/null +++ b/assets/images/video/danmu_open.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 9626ef082..20bbcd149 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:PiliPalaX/http/danmaku.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -14,6 +15,7 @@ import 'package:PiliPalaX/plugin/pl_player/index.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/utils/utils.dart'; import 'package:PiliPalaX/utils/video_utils.dart'; +import 'package:ns_danmaku/models/danmaku_item.dart'; import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; @@ -147,6 +149,85 @@ class VideoDetailController extends GetxController oid.value = IdUtils.bv2av(Get.parameters['bvid']!); } + /// 发送弹幕 + void showShootDanmakuSheet() { + final TextEditingController textController = TextEditingController(); + bool isSending = false; // 追踪是否正在发送 + showDialog( + context: Get.context!, + builder: (BuildContext context) { + // TODO: 支持更多类型和颜色的弹幕 + return AlertDialog( + title: const Text('发送弹幕'), + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextField( + controller: textController, + autofocus: true, + ); + }), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextButton( + onPressed: isSending + ? null + : () async { + final String msg = textController.text; + if (msg.isEmpty) { + SmartDialog.showToast('弹幕内容不能为空'); + return; + } else if (msg.length > 100) { + SmartDialog.showToast('弹幕内容不能超过100个字符'); + return; + } + isSending = true; // 开始发送,更新状态 + //修改按钮文字 + // SmartDialog.showToast('弹幕发送中,\n$msg'); + final dynamic res = await DanmakaHttp.shootDanmaku( + oid: cid.value, + msg: textController.text, + bvid: bvid, + progress: + plPlayerController.position.value.inMilliseconds, + type: 1, + ); + isSending = false; // 发送结束,更新状态 + if (res['status']) { + SmartDialog.showToast('发送成功'); + // 发送成功,自动预览该弹幕,避免重新请求 + // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 + plPlayerController.danmakuController!.addItems([ + DanmakuItem( + msg, + color: Colors.white, + time: plPlayerController + .position.value.inMilliseconds, + type: DanmakuItemType.scroll, + isSend: true, + ) + ]); + Get.back(); + } else { + SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); + } + }, + child: Text(isSending ? '发送中...' : '发送'), + ); + }) + ], + ); + }, + ); + } + /// 更新画质、音质 /// TODO 继续进度播放 updatePlayer() { diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 7721d41d5..43faf9d91 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -19,6 +19,7 @@ import 'package:auto_orientation/auto_orientation.dart'; import 'package:floating/floating.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -1162,6 +1163,52 @@ class _VideoDetailPageState extends State ), ), ), + Flexible( + flex: 1, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 32, + child: TextButton( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero), + ), + onPressed: videoDetailController.showShootDanmakuSheet, + child: + const Text('发弹幕', style: TextStyle(fontSize: 12)), + ), + ), + SizedBox( + width: 38, + height: 38, + child: Obx( + () => IconButton( + onPressed: () { + if (plPlayerController != null) { + videoDetailController + .plPlayerController.isOpenDanmu.value = + !videoDetailController + .plPlayerController.isOpenDanmu.value; + } + }, + icon: SvgPicture.asset( + videoDetailController + .plPlayerController.isOpenDanmu.value + ? 'assets/images/video/danmu_open.svg' + : 'assets/images/video/danmu_close.svg', + // ignore: deprecated_member_use + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ), + const SizedBox(width: 14), + ], + ), + ), + ), ], ), ), diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index aecc8f431..c55dac199 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -20,7 +20,6 @@ import 'package:PiliPalaX/pages/video/detail/introduction/widgets/menu_row.dart' import 'package:PiliPalaX/plugin/pl_player/index.dart'; import 'package:PiliPalaX/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPalaX/utils/storage.dart'; -import 'package:PiliPalaX/http/danmaku.dart'; import 'package:PiliPalaX/services/shutdown_timer_service.dart'; import '../../../../models/video/play/CDN.dart'; import '../../../../models/video_detail_res.dart'; @@ -117,7 +116,7 @@ class _HeaderControlState extends State { height: 500, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -154,7 +153,7 @@ class _HeaderControlState extends State { // trailing: Transform.scale( // scale: 0.75, // child: Switch( - // thumbIcon: MaterialStateProperty.resolveWith( + // thumbIcon: WidgetStateProperty.resolveWith( // (Set states) { // if (states.isNotEmpty && // states.first == MaterialState.selected) { @@ -461,88 +460,6 @@ class _HeaderControlState extends State { ); } - /// 发送弹幕 - void showShootDanmakuSheet() { - final TextEditingController textController = TextEditingController(); - bool isSending = false; // 追踪是否正在发送 - showDialog( - context: Get.context!, - builder: (BuildContext context) { - // TODO: 支持更多类型和颜色的弹幕 - return AlertDialog( - title: const Text('发送弹幕'), - content: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextField( - controller: textController, - ); - }), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - '取消', - style: TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return TextButton( - onPressed: isSending - ? null - : () async { - final String msg = textController.text; - if (msg.isEmpty) { - SmartDialog.showToast('弹幕内容不能为空'); - return; - } else if (msg.length > 100) { - SmartDialog.showToast('弹幕内容不能超过100个字符'); - return; - } - setState(() { - isSending = true; // 开始发送,更新状态 - }); - //修改按钮文字 - // SmartDialog.showToast('弹幕发送中,\n$msg'); - final dynamic res = await DanmakaHttp.shootDanmaku( - oid: widget.videoDetailCtr!.cid.value, - msg: textController.text, - bvid: widget.videoDetailCtr!.bvid, - progress: - widget.controller!.position.value.inMilliseconds, - type: 1, - ); - setState(() { - isSending = false; // 发送结束,更新状态 - }); - if (res['status']) { - SmartDialog.showToast('发送成功'); - // 发送成功,自动预览该弹幕,避免重新请求 - // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 - widget.controller!.danmakuController!.addItems([ - DanmakuItem( - msg, - color: Colors.white, - time: widget - .controller!.position.value.inMilliseconds, - type: DanmakuItemType.scroll, - isSend: true, - ) - ]); - Get.back(); - } else { - SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); - } - }, - child: Text(isSending ? '发送中...' : '发送'), - ); - }) - ], - ); - }, - ); - } - /// 定时关闭 void scheduleExit() async { const List scheduleTimeChoices = [ @@ -564,7 +481,7 @@ class _HeaderControlState extends State { height: 500, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -624,7 +541,7 @@ class _HeaderControlState extends State { inactiveThumbColor: Theme.of(context).colorScheme.primaryContainer, inactiveTrackColor: - Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.surface, splashRadius: 10.0, // boolean variable value value: shutdownTimerService.waitForPlayingCompleted, @@ -704,7 +621,7 @@ class _HeaderControlState extends State { height: 310, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -803,7 +720,7 @@ class _HeaderControlState extends State { height: 250, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -889,7 +806,7 @@ class _HeaderControlState extends State { height: 250, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -985,7 +902,7 @@ class _HeaderControlState extends State { height: 580, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -1345,7 +1262,7 @@ class _HeaderControlState extends State { height: 300, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), ), margin: const EdgeInsets.all(12), @@ -1529,11 +1446,11 @@ class _HeaderControlState extends State { child: IconButton( tooltip: '发弹幕', style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), + padding: WidgetStateProperty.all(EdgeInsets.zero), ), - onPressed: () => showShootDanmakuSheet(), + onPressed: widget.videoDetailCtr?.showShootDanmakuSheet, icon: const Icon( - Icons.add_comment_outlined, + Icons.comment_outlined, size: 19, color: Colors.white, ), @@ -1558,8 +1475,8 @@ class _HeaderControlState extends State { }, icon: Icon( _.isOpenDanmu.value - ? Icons.comment_outlined - : Icons.comments_disabled_outlined, + ? Icons.subtitles_outlined + : Icons.subtitles_off_outlined, size: 19, color: Colors.white, ), @@ -1612,7 +1529,7 @@ class _HeaderControlState extends State { TextButton( style: ButtonStyle( foregroundColor: - MaterialStateProperty.resolveWith( + WidgetStateProperty.resolveWith( (states) { return Theme.of(context) .snackBarTheme @@ -1629,7 +1546,7 @@ class _HeaderControlState extends State { TextButton( style: ButtonStyle( foregroundColor: - MaterialStateProperty.resolveWith( + WidgetStateProperty.resolveWith( (states) { return Theme.of(context) .snackBarTheme @@ -1669,7 +1586,7 @@ class _HeaderControlState extends State { child: IconButton( tooltip: "更多设置", style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), + padding: WidgetStateProperty.all(EdgeInsets.zero), ), onPressed: () => showSettingSheet(), icon: const Icon( diff --git a/pubspec.yaml b/pubspec.yaml index 5c2579eb9..3bf6aa121 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -249,6 +249,7 @@ flutter: - assets/images/lv/ - assets/images/logo/ - assets/images/live/ + - assets/images/video/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware