diff --git a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart index c67bbb00..b699c4af 100644 --- a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart +++ b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class CustomSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { - CustomSliverPersistentHeaderDelegate({ + const CustomSliverPersistentHeaderDelegate({ required this.child, required this.bgColor, double extent = 45, diff --git a/lib/common/widgets/dialog/report.dart b/lib/common/widgets/dialog/report.dart index 851461a7..ee530fe3 100644 --- a/lib/common/widgets/dialog/report.dart +++ b/lib/common/widgets/dialog/report.dart @@ -231,4 +231,34 @@ class ReportOptions { 0: '其他', }, }; + + static Map> get danmakuReport => const { + '': { + 1: '违法违禁', + 2: '色情低俗', + 3: '赌博诈骗', + 4: '人身攻击', + 5: '侵犯隐私', + 6: '垃圾广告', + 7: '引战', + 8: '剧透', + 9: '恶意刷屏', + 10: '视频无关', + 12: '青少年不良信息', + 13: '违法信息外链', + 0: '其它', // 11 + }, + }; + + static Map> get liveDanmakuReport => const { + '': { + 1: '违法违规', + 2: '低俗色情', + 3: '垃圾广告', + 4: '辱骂引战', + 5: '政治敏感', + 6: '青少年不良信息', + 7: '其他 ', // avoid show form + }, + }; } diff --git a/lib/http/api.dart b/lib/http/api.dart index 72bf8b03..dbc98d13 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -955,4 +955,15 @@ class Api { static const String popularPrecious = '/x/web-interface/popular/precious'; static const String userRealName = '/x/member/app/up/realname'; + + static const String liveDmReport = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/v1/dMReport/Report'; + + static const String danmakuLike = '/x/v2/dm/thumbup/add'; + + static const String danmakuReport = '/x/dm/report/add'; + + static const String danmakuRecall = '/x/dm/recall'; + + static const String danmakuEditState = '/x/v2/dm/edit/state'; } diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart index 6314b1dd..8370bed9 100644 --- a/lib/http/danmaku.dart +++ b/lib/http/danmaku.dart @@ -1,9 +1,10 @@ import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:dio/dio.dart'; -class DanmakuHttp { +abstract final class DanmakuHttp { static Future shootDanmaku({ int type = 1, //弹幕类选择(1:视频弹幕 2:漫画弹幕) required int oid, // 视频cid @@ -27,23 +28,23 @@ class DanmakuHttp { // assert(aid != null || bvid != null); // assert(csrf != null || access_key != null); // 构建参数对象 - var data = { + var data = { 'type': type, 'oid': oid, 'msg': msg, 'mode': mode, //'aid': aid, 'bvid': bvid, - 'progress': progress, - 'color': colorful ? 16777215 : color, - 'fontsize': fontsize, - 'pool': pool, + 'progress': ?progress, + 'color': ?colorful ? 16777215 : color, + 'fontsize': ?fontsize, + 'pool': ?pool, 'rnd': DateTime.now().microsecondsSinceEpoch, - 'colorful': colorful ? 60001 : null, - 'checkbox_type': checkboxType, + 'colorful': ?colorful ? 60001 : null, + 'checkbox_type': ?checkboxType, 'csrf': Accounts.main.csrf, // 'access_key': access_key, - }..removeWhere((key, value) => value == null); + }; var response = await Request().post( Api.shootDanmaku, @@ -68,4 +69,132 @@ class DanmakuHttp { }; } } + + static Future> danmakuLike({ + required bool isLike, + required int cid, + required int id, + }) async { + final data = { + 'op': isLike ? 2 : 1, + 'dmid': id, + 'oid': cid, + 'platform': 'web_player', + 'polaris_app_id': 100, + 'polaris_platform': 5, + 'spmid': '333.788.0.0', + 'from_spmid': '333.788.0.0', + 'statistics': '{"appId":100,"platform":5,"abtest":"","version":""}', + 'csrf': Accounts.main.csrf, + }; + final res = await Request().post( + Api.danmakuLike, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return const Success(null); + } else { + return Error(res.data['message']); + } + } + + static Future> danmakuReport({ + required int reason, + required int cid, + required int id, + bool block = false, + String? content, + }) async { + final data = { + 'cid': cid, + 'dmid': id, + 'reason': reason, + 'block': block, + 'originCid': cid, + 'content': ?content, + 'polaris_app_id': 100, + 'polaris_platform': 5, + 'spmid': '333.788.0.0', + 'from_spmid': '333.788.0.0', + 'statistics': '{"appId":100,"platform":5,"abtest":"","version":""}', + 'csrf': Accounts.main.csrf, + }; + final res = await Request().post( + Api.danmakuReport, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data']['block'], + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + + /// { + /// 0: "举报已提交", + /// "-1": "举报失败,请先激活账号。", + /// "-2": "举报失败,系统拒绝受理您的举报请求。", + /// "-3": "举报失败,您已经被禁言。", + /// "-4": "您的操作过于频繁,请稍后再试。", + /// "-5": "您已经举报过这条弹幕了。", + /// "-6": "举报失败,系统错误。" + /// } + } + } + + static Future> danmakuRecall({ + required int cid, + required int id, + }) async { + final data = { + 'dmid': id, + 'cid': cid, + 'type': 1, + 'csrf': Accounts.main.csrf, + }; + final res = await Request().post( + Api.danmakuRecall, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return Success(res.data['message']); + } else { + return Error(res.data['message']); + } + } + + static Future> danmakuEditState({ + required int oid, + required Iterable ids, + required int state, + }) async { + /// 0: 取消删除 + /// 1:删除弹幕 + /// 2:弹幕保护 + /// 3:取消保护 + final data = { + 'dmids': ids.join(','), + 'oid': oid, + 'state': state, + 'type': 1, + 'csrf': Accounts.main.csrf, + }; + final res = await Request().post( + Api.danmakuRecall, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return Success(res.data['message']); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/http/live.dart b/lib/http/live.dart index c4f8c7d2..8e438e03 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -666,4 +666,41 @@ abstract final class LiveHttp { return Error(res.data['message']); } } + + static Future> liveDmReport({ + required Object roomId, + required int mid, + required String msg, + required String reason, + required int reasonId, + required String id, + }) async { + final csrf = Accounts.main.csrf; + final data = { + 'id': 0, + 'roomid': roomId, + 'tuid': mid, + 'msg': msg, + 'reason': reason, + 'sign': '', + 'reason_id': reasonId, + 'token': '', + 'dm_type': '0', + 'id_str': id, + 'csrf_token': csrf, + 'csrf': csrf, + 'visit_id': '', + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + final res = await Request().post( + Api.liveDmReport, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return const Success(null); // {"id": num} + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/pages/danmaku/dnamaku_model.dart b/lib/pages/danmaku/dnamaku_model.dart new file mode 100644 index 00000000..820f0363 --- /dev/null +++ b/lib/pages/danmaku/dnamaku_model.dart @@ -0,0 +1,27 @@ +sealed class DanmakuExtra { + String get mid; + Object get id; + + const DanmakuExtra(); +} + +class VideoDanmaku extends DanmakuExtra { + @override + final int id; + @override + final String mid; + + bool isLike; + + VideoDanmaku({required this.id, required this.mid, this.isLike = false}); +} + +class LiveDanmaku extends DanmakuExtra { + @override + final String id; + @override + final String mid; + final String uname; + + const LiveDanmaku({required this.id, required this.mid, required this.uname}); +} diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index b0b6ea45..2d8b98ac 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart'; import 'package:PiliPlus/pages/danmaku/controller.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/utils/danmaku_utils.dart'; @@ -32,7 +33,7 @@ class _PlDanmakuState extends State { PlPlayerController get playerController => widget.playerController; late PlDanmakuController _plDanmakuController; - DanmakuController? _controller; + DanmakuController? _controller; int latestAddedPosition = -1; @override @@ -130,6 +131,7 @@ class _PlDanmakuState extends State { e.colorful == DmColorfulType.VipGradualColor, count: e.hasCount() ? e.count : null, selfSend: e.isSelf, + extra: VideoDanmaku(id: e.id.toInt(), mid: e.midHash), ), ); } @@ -154,8 +156,8 @@ class _PlDanmakuState extends State { ? playerController.danmakuOpacity.value : 0, duration: const Duration(milliseconds: 100), - child: DanmakuScreen( - createdController: (DanmakuController e) { + child: DanmakuScreen( + createdController: (e) { playerController.danmakuController = _controller = e; }, option: DanmakuOption( diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 60a69971..99cc41c7 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -12,6 +12,7 @@ import 'package:PiliPlus/models_new/live/live_dm_info/data.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/live_room/send_danmaku/view.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; @@ -36,7 +37,7 @@ class LiveRoomController extends GetxController { final String heroTag; int roomId = Get.arguments; - DanmakuController? danmakuController; + DanmakuController? danmakuController; PlPlayerController plPlayerController = PlPlayerController.getInstance( isLive: true, ); @@ -340,7 +341,9 @@ class LiveRoomController extends GetxController { final info = obj['info']; final first = info[0]; final content = first[15]; - final extra = jsonDecode(content['extra']); + final Map extra = jsonDecode( + content['extra'], + ); final user = content['user']; final uid = user['uid']; BaseEmote? uemote; @@ -368,6 +371,11 @@ class LiveRoomController extends GetxController { : DmUtils.decimalToColor(extra['color']), type: DmUtils.getPosition(extra['mode']), selfSend: extra['send_from_me'] ?? false, + extra: LiveDanmaku( + id: extra['id_str'], + mid: uid, + uname: user['base']['name'], + ), ), ); if (!disableAutoScroll.value) { diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 360b106c..0a4200ec 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/live_room/controller.dart'; import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart'; import 'package:PiliPlus/pages/live_room/superchat/superchat_panel.dart'; @@ -1039,8 +1040,8 @@ class _LiveDanmakuState extends State { ? plPlayerController.danmakuOpacity.value : 0, duration: const Duration(milliseconds: 100), - child: DanmakuScreen( - createdController: (DanmakuController e) { + child: DanmakuScreen( + createdController: (e) { widget.liveRoomController.danmakuController = plPlayerController.danmakuController = e; }, diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index 56d63ed0..1dc3cc6f 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -6,7 +6,9 @@ import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/pages/common/publish/common_text_pub_page.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; +import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:canvas_danmaku/models/danmaku_content_item.dart'; import 'package:flutter/material.dart'; @@ -20,7 +22,7 @@ class SendDanmakuPanel extends CommonTextPubPage { final dynamic bvid; final dynamic progress; - final ValueChanged callback; + final ValueChanged> callback; final bool darkVideoPage; // config @@ -473,6 +475,10 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { }, selfSend: true, isColorful: isColorful, + extra: VideoDanmaku( + id: res['dmid'], + mid: PlPlayerController.instance!.midHash, + ), ), ); } else { diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index ce165ba0..03faf808 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -5,7 +5,11 @@ import 'dart:typed_data'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; +import 'package:PiliPlus/http/danmaku.dart'; +import 'package:PiliPlus/http/danmaku_block.dart'; import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/cdn_type.dart'; @@ -13,6 +17,7 @@ import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/pages/video/controller.dart'; @@ -407,6 +412,15 @@ class HeaderControlState extends State { style: subTitleStyle, ), ), + ListTile( + dense: true, + onTap: () { + Get.back(); + showDanmakuPool(); + }, + leading: const Icon(CustomIcons.dm_on, size: 20), + title: const Text('弹幕列表', style: titleStyle), + ), ListTile( dense: true, onTap: () { @@ -1337,7 +1351,7 @@ class HeaderControlState extends State { int danmakuFontWeight = plPlayerController.danmakuFontWeight; bool massiveMode = plPlayerController.massiveMode; - final DanmakuController? danmakuController = + final DanmakuController? danmakuController = plPlayerController.danmakuController; showBottomSheet( @@ -1843,6 +1857,146 @@ class HeaderControlState extends State { ); } + void showDanmakuPool() { + final ctr = plPlayerController.danmakuController; + if (ctr == null) return; + showBottomSheet((context, setState) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.all(12), + child: Material( + clipBehavior: Clip.hardEdge, + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: CustomScrollView( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('弹幕列表'), + IconButton( + onPressed: () => setState(() {}), + icon: const Icon(Icons.refresh), + ), + ], + ), + ), + bgColor: null, + ), + ), + ?_buildDanmakuList(ctr.staticDanmaku), + ?_buildDanmakuList(ctr.scrollDanmaku), + ?_buildDanmakuList(ctr.specialDanmaku), + ], + ), + ), + ), + ); + }); + } + + Widget? _buildDanmakuList(List> list) { + if (list.isEmpty) return null; + list = List.of(list); + + return SliverList.builder( + itemCount: list.length, + itemBuilder: (context, index) { + final item = list[index]; + final extra = item.content.extra! as VideoDanmaku; + return ListTile( + onLongPress: () => Utils.copyText(item.content.text), + subtitle: Text(item.content.text), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Builder( + builder: (context) => IconButton( + onPressed: () async { + if (!Accounts.main.isLogin) { + SmartDialog.showToast('请先登录'); + return; + } + final res = await DanmakuHttp.danmakuLike( + isLike: extra.isLike, + cid: plPlayerController.cid!, + id: extra.id, + ); + if (res.isSuccess) { + extra.isLike = !extra.isLike; + if (context.mounted) { + (context as Element).markNeedsBuild(); + } + } else { + res.toast(); + } + }, + icon: extra.isLike + ? const Icon(Icons.thumb_up) + : const Icon(Icons.thumb_up_outlined), + ), + ), + if (item.content.selfSend) + IconButton( + onPressed: () async { + final res = await DanmakuHttp.danmakuRecall( + cid: plPlayerController.cid!, + id: extra.id, + ); + if (res.isSuccess) { + SmartDialog.showToast('删除成功'); + } else { + res.toast(); + } + }, + icon: const Icon(Icons.delete_outline), + ) + else + IconButton( + onPressed: () { + autoWrapReportDialog( + context, + ReportOptions.danmakuReport, + (reasonType, reasonDesc, banUid) { + if (banUid) { + final filter = plPlayerController.filters; + if (filter.dmUid.add(extra.mid)) { + filter.count++; + GStorage.localCache.put( + LocalCacheKey.danmakuFilterRules, + filter, + ); + } + DanmakuFilterHttp.danmakuFilterAdd( + filter: extra.mid, + type: 2, + ); + } + return DanmakuHttp.danmakuReport( + reason: reasonType == 0 ? 11 : reasonType, + cid: plPlayerController.cid!, + id: extra.id, + content: reasonDesc, + ); + }, + ); + }, + icon: const Icon(Icons.report_problem_outlined), + ), + ], + ), + ); + }, + ); + } + /// 播放顺序 void showSetRepeat() { showBottomSheet( diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 5ddefcfe..2f1082ae 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -17,6 +17,7 @@ import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; @@ -326,7 +327,7 @@ class PlPlayerController { late int danmakuWeight = Pref.danmakuWeight; late RuleFilter filters = Pref.danmakuFilterRule; // 关联弹幕控制器 - DanmakuController? danmakuController; + DanmakuController? danmakuController; bool showDanmaku = true; Set dmState = {}; late final mergeDanmaku = Pref.mergeDanmaku;