feat: danmaku api (#1530)

This commit is contained in:
My-Responsitories
2025-10-12 18:41:40 +08:00
committed by GitHub
parent 88d207cc24
commit e5f0742bf6
12 changed files with 426 additions and 20 deletions

View File

@@ -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,

View File

@@ -231,4 +231,34 @@ class ReportOptions {
0: '其他',
},
};
static Map<String, Map<int, String>> get danmakuReport => const {
'': {
1: '违法违禁',
2: '色情低俗',
3: '赌博诈骗',
4: '人身攻击',
5: '侵犯隐私',
6: '垃圾广告',
7: '引战',
8: '剧透',
9: '恶意刷屏',
10: '视频无关',
12: '青少年不良信息',
13: '违法信息外链',
0: '其它', // 11
},
};
static Map<String, Map<int, String>> get liveDanmakuReport => const {
'': {
1: '违法违规',
2: '低俗色情',
3: '垃圾广告',
4: '辱骂引战',
5: '政治敏感',
6: '青少年不良信息',
7: '其他 ', // avoid show form
},
};
}

View File

@@ -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';
}

View File

@@ -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 = <String, dynamic>{
var data = <String, Object>{
'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<LoadingState<Null>> 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<Map<String, dynamic>> 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<LoadingState<String?>> 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<LoadingState<String?>> danmakuEditState({
required int oid,
required Iterable<int> 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']);
}
}
}

View File

@@ -666,4 +666,41 @@ abstract final class LiveHttp {
return Error(res.data['message']);
}
}
static Future<LoadingState<Null>> 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']);
}
}
}

View File

@@ -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});
}

View File

@@ -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<PlDanmaku> {
PlPlayerController get playerController => widget.playerController;
late PlDanmakuController _plDanmakuController;
DanmakuController? _controller;
DanmakuController<DanmakuExtra>? _controller;
int latestAddedPosition = -1;
@override
@@ -130,6 +131,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
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<PlDanmaku> {
? playerController.danmakuOpacity.value
: 0,
duration: const Duration(milliseconds: 100),
child: DanmakuScreen(
createdController: (DanmakuController e) {
child: DanmakuScreen<DanmakuExtra>(
createdController: (e) {
playerController.danmakuController = _controller = e;
},
option: DanmakuOption(

View File

@@ -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<DanmakuExtra>? 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<String, dynamic> 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) {

View File

@@ -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<LiveDanmaku> {
? plPlayerController.danmakuOpacity.value
: 0,
duration: const Duration(milliseconds: 100),
child: DanmakuScreen(
createdController: (DanmakuController e) {
child: DanmakuScreen<DanmakuExtra>(
createdController: (e) {
widget.liveRoomController.danmakuController =
plPlayerController.danmakuController = e;
},

View File

@@ -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<DanmakuContentItem> callback;
final ValueChanged<DanmakuContentItem<DanmakuExtra>> callback;
final bool darkVideoPage;
// config
@@ -473,6 +475,10 @@ class _SendDanmakuPanelState extends CommonTextPubPageState<SendDanmakuPanel> {
},
selfSend: true,
isColorful: isColorful,
extra: VideoDanmaku(
id: res['dmid'],
mid: PlPlayerController.instance!.midHash,
),
),
);
} else {

View File

@@ -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<HeaderControl> {
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<HeaderControl> {
int danmakuFontWeight = plPlayerController.danmakuFontWeight;
bool massiveMode = plPlayerController.massiveMode;
final DanmakuController? danmakuController =
final DanmakuController<DanmakuExtra>? danmakuController =
plPlayerController.danmakuController;
showBottomSheet(
@@ -1843,6 +1857,146 @@ class HeaderControlState extends State<HeaderControl> {
);
}
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<DanmakuItem<DanmakuExtra>> 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(

View File

@@ -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<DanmakuExtra>? danmakuController;
bool showDanmaku = true;
Set<int> dmState = <int>{};
late final mergeDanmaku = Pref.mergeDanmaku;