diff --git a/lib/http/api.dart b/lib/http/api.dart index 0125c742..04fe0a44 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -312,6 +312,10 @@ class Api { static const String webDanmaku = '/x/v2/dm/web/seg.so'; + //发送视频弹幕 + //https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/action.md + static const String shootDanmaku = '/x/v2/dm/post'; + // up主分组 static const String followUpTag = '/x/relation/tags'; diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart index 87f08d8b..e34320e7 100644 --- a/lib/http/danmaku.dart +++ b/lib/http/danmaku.dart @@ -24,4 +24,72 @@ class DanmakaHttp { ); return DmSegMobileReply.fromBuffer(response.data); } + static Future shootDanmaku({ + int type = 1,//弹幕类选择(1:视频弹幕 2:漫画弹幕) + required int oid,// 视频cid + required String msg,//弹幕文本(长度小于 100 字符) + int mode = 1,// 弹幕类型(1:滚动弹幕 4:底端弹幕 5:顶端弹幕 6:逆向弹幕(不能使用) 7:高级弹幕 8:代码弹幕(不能使用) 9:BAS弹幕(pool必须为2)) + // String? aid,// 稿件avid + // String? bvid,// bvid与aid必须有一个 + required String bvid, + int? progress,// 弹幕出现在视频内的时间(单位为毫秒,默认为0) + int? color,// 弹幕颜色(默认白色,16777215) + int? fontsize,// 弹幕字号(默认25) + int? pool,// 弹幕池选择(0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕)默认普通池,0) + //int? rnd,// 当前时间戳*1000000(若无此项,则发送弹幕冷却时间限制为90s;若有此项,则发送弹幕冷却时间限制为5s) + int? colorful,//60001:专属渐变彩色(需要会员) + int? checkbox_type,//是否带 UP 身份标识(0:普通;4:带有标识) + // String? csrf,//CSRF Token(位于 Cookie) Cookie 方式必要 + // String? access_key,// APP 登录 Token APP 方式必要 + }) async { + // 构建参数对象 + // assert(aid != null || bvid != null); + // assert(csrf != null || access_key != null); + assert(msg.length < 100); + // 构建参数对象 + var params = { + 'type': type, + 'oid': oid, + 'msg': msg, + 'mode': mode, + //'aid': aid, + 'bvid': bvid, + 'progress': progress, + 'color': color, + 'fontsize': fontsize, + 'pool': pool, + 'rnd': DateTime.now().microsecondsSinceEpoch, + 'colorful': colorful, + 'checkbox_type': checkbox_type, + 'csrf': await Request.getCsrf(), + // 'access_key': access_key, + }..removeWhere((key, value) => value == null); + + var response = await Request().post( + Api.shootDanmaku, + data: params, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (response.statusCode != 200) { + return { + 'status': false, + 'data': [], + 'msg': '弹幕发送失败,状态码:${response.statusCode}', + }; + } + if (response.data['code'] == 0) { + return { + 'status': true, + 'data': response.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': "${response.data['code']}: ${response.data['message']}", + }; + } + } } diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index ee5f2c0f..6068becf 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -15,6 +15,7 @@ import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; +import 'package:pilipala/http/danmaku.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { final PlPlayerController? controller; @@ -179,6 +180,84 @@ class _HeaderControlState extends State { ); } + /// 发送弹幕 + void showShootDanmakuSheet() { + final TextEditingController textController = TextEditingController(); + bool isSending = false; // 追踪是否正在发送 + showDialog( + context: Get.context!, + builder: (context) { + // TODO: 支持更多类型和颜色的弹幕 + return AlertDialog( + title: const Text('发送弹幕(测试)'), + content: StatefulBuilder(builder: (context, StateSetter setState) { + return TextField( + controller: textController, + ); + }), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + StatefulBuilder(builder: (context, StateSetter setState) { + return TextButton( + onPressed: isSending + ? null + : () async { + 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'); + 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, + ) + ]); + Get.back(); + } else { + SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); + } + }, + child: Text(isSending ? '发送中...' : '发送'), + ); + }) + ], + ); + }, + ); + } + /// 选择倍速 void showSetSpeedSheet() { double currentSpeed = widget.controller!.playbackSpeed; @@ -825,6 +904,20 @@ class _HeaderControlState extends State { // ), // fuc: () => _.screenshot(), // ), + SizedBox( + width: 56, + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => showShootDanmakuSheet(), + child: const Text( + '发弹幕', + style: textStyle, + ), + ), + ), SizedBox( width: 34, height: 34,