live dm action

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-10-16 17:35:54 +08:00
parent 3fb9e22378
commit 6c361a047b
7 changed files with 208 additions and 276 deletions

View File

@@ -260,7 +260,7 @@ class ReportOptions {
4: '辱骂引战',
5: '政治敏感',
6: '青少年不良信息',
7: '其他 ', // avoid show form
7: '其他', // avoid show form
},
};
}

View File

@@ -1,159 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_svg/flutter_svg.dart';
/// https://github.com/yang-f/flutter_svg_provider
/// Rasterizes given svg picture for displaying in [Image] widget:
///
/// ```dart
/// Image(
/// width: 32,
/// height: 32,
/// image: Svg('assets/my_icon.svg'),
/// )
/// ```
class SvgImageProvider extends ImageProvider<SvgImageKey> {
/// Path to svg file or asset
final String path;
/// Size in logical pixels to render.
/// Useful for [DecorationImage].
/// If not specified, will use size from [Image].
/// If [Image] not specifies size too, will use default size 100x100.
final Size? size;
/// Color to tint the SVG
final Color? color;
/// Image scale.
final double? scale;
/// Width and height can also be specified from [Image] constructor.
/// Default size is 100x100 logical pixels.
/// Different size can be specified in [Image] parameters
const SvgImageProvider(
this.path, {
this.size,
this.scale,
this.color,
});
@override
Future<SvgImageKey> obtainKey(ImageConfiguration configuration) {
final Color color = this.color ?? Colors.transparent;
final double scale = this.scale ?? configuration.devicePixelRatio ?? 1.0;
final double logicWidth = size?.width ?? configuration.size?.width ?? 100;
final double logicHeight =
size?.height ?? configuration.size?.height ?? 100;
return SynchronousFuture<SvgImageKey>(
SvgImageKey(
path: path,
scale: scale,
color: color,
pixelWidth: (logicWidth * scale).round(),
pixelHeight: (logicHeight * scale).round(),
),
);
}
@override
ImageStreamCompleter loadImage(SvgImageKey key, ImageDecoderCallback decode) {
return OneFrameImageStreamCompleter(
_loadAsync(key, getFilterColor(color)),
);
}
static Future<ImageInfo> _loadAsync(SvgImageKey key, Color color) async {
final rawSvg = await rootBundle.loadString(key.path);
final pictureInfo = await vg.loadPicture(
SvgStringLoader(rawSvg, theme: SvgTheme(currentColor: color)),
null,
clipViewbox: false,
);
try {
final image = pictureInfo.picture.toImageSync(
pictureInfo.size.width.round(),
pictureInfo.size.height.round(),
);
return ImageInfo(image: image);
} finally {
// Dispose of the Picture to release resources
pictureInfo.picture.dispose();
}
}
// Note: == and hashCode not overrided as changes in properties
// (width, height and scale) are not observable from the here.
// [SvgImageKey] instances will be compared instead.
@override
String toString() => '$runtimeType(${describeIdentity(path)})';
// Running on web with Colors.transparent may throws the exception `Expected a value of type 'SkDeletable', but got one of type 'Null'`.
static Color getFilterColor(Color? color) {
if (kIsWeb && color == Colors.transparent) {
return const Color(0x01ffffff);
} else {
return color ?? Colors.transparent;
}
}
}
@immutable
class SvgImageKey {
const SvgImageKey({
required this.path,
required this.pixelWidth,
required this.pixelHeight,
required this.scale,
this.color,
});
/// Path to svg asset.
final String path;
/// Width in physical pixels.
/// Used when raterizing.
final int pixelWidth;
/// Height in physical pixels.
/// Used when raterizing.
final int pixelHeight;
/// Color to tint the SVG
final Color? color;
/// Used to calculate logical size from physical, i.e.
/// logicalWidth = [pixelWidth] / [scale],
/// logicalHeight = [pixelHeight] / [scale].
/// Should be equal to [MediaQueryData.devicePixelRatio].
final double scale;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SvgImageKey &&
other.path == path &&
other.pixelWidth == pixelWidth &&
other.pixelHeight == pixelHeight &&
other.scale == scale &&
other.color == color;
}
@override
int get hashCode => Object.hash(
path,
pixelWidth,
pixelHeight,
scale,
color,
);
}

View File

@@ -667,13 +667,16 @@ abstract final class LiveHttp {
}
}
static Future<LoadingState<Null>> liveDmReport({
required Object roomId,
required int mid,
static Future<Map<String, dynamic>> liveDmReport({
required int roomId,
required Object mid,
required String msg,
required String reason,
required int reasonId,
required String id,
required int dmType,
required Object idStr,
required Object ts,
required Object sign,
}) async {
final csrf = Accounts.main.csrf;
final data = {
@@ -682,25 +685,21 @@ abstract final class LiveHttp {
'tuid': mid,
'msg': msg,
'reason': reason,
'sign': '',
'ts': ts,
'sign': sign,
'reason_id': reasonId,
'token': '',
'dm_type': '0',
'id_str': id,
'dm_type': dmType,
'id_str': idStr,
'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']);
}
return res.data as Map<String, dynamic>;
}
}

View File

@@ -21,7 +21,17 @@ class LiveDanmaku extends DanmakuExtra {
final Object id;
@override
final Object mid;
final String uname;
const LiveDanmaku({required this.id, required this.mid, required this.uname});
final int dmType;
final Object ts;
final Object ct;
const LiveDanmaku({
required this.id,
required this.mid,
required this.dmType,
required this.ts,
required this.ct,
});
}

View File

@@ -345,16 +345,19 @@ class LiveRoomController extends GetxController {
content['extra'],
);
final user = content['user'];
// final midHash = first[7];
final uid = user['uid'];
final name = user['base']['name'];
final msg = info[1];
BaseEmote? uemote;
if (first[13] case Map<String, dynamic> map) {
uemote = BaseEmote.fromJson(map);
}
messages.add(
DanmakuMsg(
name: user['base']['name'],
name: name,
uid: uid,
text: info[1],
text: msg,
emots: (extra['emots'] as Map<String, dynamic>?)?.map(
(k, v) => MapEntry(k, BaseEmote.fromJson(v)),
),
@@ -363,9 +366,10 @@ class LiveRoomController extends GetxController {
);
if (plPlayerController.showDanmaku) {
plPlayerController.danmakuController?.addDanmaku(
final checkInfo = info[9];
danmakuController?.addDanmaku(
DanmakuContentItem(
extra['content'],
msg,
color: plPlayerController.blockColorful
? Colors.white
: DmUtils.decimalToColor(extra['color']),
@@ -374,7 +378,9 @@ class LiveRoomController extends GetxController {
extra: LiveDanmaku(
id: extra['id_str'],
mid: uid,
uname: user['base']['name'],
dmType: extra['dm_type'],
ts: checkInfo['ts'],
ct: checkInfo['ct'],
),
),
);

View File

@@ -11,6 +11,7 @@ import 'package:PiliPlus/common/widgets/marquee.dart';
import 'package:PiliPlus/http/danmaku.dart';
import 'package:PiliPlus/http/danmaku_block.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/live.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';
@@ -105,10 +106,10 @@ class HeaderControl extends StatefulWidget {
}
static Future<void> reportDanmaku(
VideoDanmaku extra,
BuildContext context,
PlPlayerController ctr,
) {
BuildContext context, {
required VideoDanmaku extra,
required PlPlayerController ctr,
}) {
if (Accounts.main.isLogin) {
return autoWrapReportDialog(
context,
@@ -132,7 +133,51 @@ class HeaderControl extends StatefulWidget {
reason: reasonType == 0 ? 11 : reasonType,
cid: ctr.cid!,
id: extra.id,
content: reasonDesc,
content: reasonType == 0 ? reasonDesc : null,
);
},
);
} else {
return SmartDialog.showToast('请先登录');
}
}
static Future<void> reportLiveDanmaku(
BuildContext context, {
required int roomId,
required String msg,
required LiveDanmaku extra,
required PlPlayerController ctr,
}) {
if (Accounts.main.isLogin) {
return autoWrapReportDialog(
context,
ReportOptions.liveDanmakuReport,
(reasonType, reasonDesc, banUid) {
// if (banUid) {
// final filter = ctr.filters;
// if (filter.dmUid.add(extra.mid)) {
// filter.count++;
// GStorage.localCache.put(
// LocalCacheKey.danmakuFilterRules,
// filter,
// );
// }
// DanmakuFilterHttp.danmakuFilterAdd(
// filter: extra.mid,
// type: 2,
// );
// }
return LiveHttp.liveDmReport(
roomId: roomId,
mid: extra.mid,
msg: msg,
reason: ReportOptions.liveDanmakuReport['']![reasonType]!,
reasonId: reasonType,
dmType: extra.dmType,
idStr: extra.id,
ts: extra.ts,
sign: extra.ct,
);
},
);
@@ -2040,9 +2085,9 @@ class HeaderControlState extends State<HeaderControl> {
else
iconButton(
onPressed: () => HeaderControl.reportDanmaku(
extra,
context,
plPlayerController,
extra: extra,
ctr: plPlayerController,
),
icon: const Icon(Icons.report_problem_outlined),
),
@@ -2326,7 +2371,7 @@ class HeaderControlState extends State<HeaderControl> {
)
: const SizedBox.shrink(),
),
if (isFSOrPip) ...[
if (isFSOrPip || Utils.isDesktop) ...[
SizedBox(
width: 42,
height: 34,

View File

@@ -7,7 +7,6 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/gesture/immediate_tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/gesture/mouse_interactive_viewer.dart';
import 'package:PiliPlus/common/widgets/image/flutter_svg_provider.dart';
import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/pair.dart';
import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart';
@@ -26,6 +25,8 @@ import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart';
import 'package:PiliPlus/models_new/video/video_shot/data.dart';
import 'package:PiliPlus/pages/common/common_intro_controller.dart';
import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart';
import 'package:PiliPlus/pages/live_room/widgets/bottom_control.dart'
as live_bottom;
import 'package:PiliPlus/pages/video/controller.dart';
import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart';
import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart';
@@ -62,6 +63,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart' hide ContextExtensionss;
@@ -2217,40 +2219,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
);
}
BoxDecoration _getDmTipBg(DanmakuItem item, double dx) {
String _getDmTipBg(DanmakuItem item) {
const offset = 65;
const size = Size(_overlayWidth, _overlayHeight);
if (item.xPosition >= maxWidth - offset) {
return const BoxDecoration(
image: DecorationImage(
filterQuality: FilterQuality.low,
image: SvgImageProvider(
'assets/images/dm_tip/player_dm_tip_right.svg',
size: size,
),
),
);
return 'right';
}
if (item.xPosition + item.width <= offset) {
return const BoxDecoration(
image: DecorationImage(
filterQuality: FilterQuality.low,
image: SvgImageProvider(
'assets/images/dm_tip/player_dm_tip_left.svg',
size: size,
),
),
);
return 'left';
}
return const BoxDecoration(
image: DecorationImage(
filterQuality: FilterQuality.low,
image: SvgImageProvider(
'assets/images/dm_tip/player_dm_tip_center.svg',
size: size,
),
),
);
return 'center';
}
Widget _buildDmAction(
@@ -2276,8 +2253,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
maxWidth - _overlaySpacing,
);
// TODO LiveDanmaku
final extra = item.content.extra as VideoDanmaku;
if (right > (maxWidth - item.xPosition)) {
_removeDmAction();
return const Positioned(left: 0, top: 0, child: SizedBox.shrink());
}
final extra = item.content.extra;
return Positioned(
right: right,
@@ -2285,63 +2266,113 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
child: SizedBox(
width: _overlayWidth,
height: _overlayHeight,
child: DecoratedBox(
decoration: _getDmTipBg(item, dx),
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_dmActionItem(
Icon(
size: 20,
extra.isLike
? CustomIcons.player_dm_tip_like_solid
: CustomIcons.player_dm_tip_like,
color: Colors.white,
),
onTap: () => HeaderControl.likeDanmaku(
extra,
plPlayerController.cid!,
),
),
_dmActionItem(
const Icon(
size: 19,
CustomIcons.player_dm_tip_copy,
color: Colors.white,
),
onTap: () => Utils.copyText(item.content.text),
),
if (item.content.selfSend)
_dmActionItem(
const Icon(
size: 20,
CustomIcons.player_dm_tip_recall,
color: Colors.white,
),
onTap: () => HeaderControl.deleteDanmaku(
extra.id,
plPlayerController.cid!,
),
)
else
_dmActionItem(
const Icon(
size: 20,
CustomIcons.player_dm_tip_back,
color: Colors.white,
),
onTap: () => HeaderControl.reportDanmaku(
extra,
context,
plPlayerController,
),
),
],
child: Stack(
children: [
SvgPicture.asset(
'assets/images/dm_tip/player_dm_tip_${_getDmTipBg(item)}.svg',
clipBehavior: Clip.none,
width: _overlayWidth,
height: _overlayHeight,
),
),
Positioned.fill(
top: 4,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: extra is VideoDanmaku
? [
_dmActionItem(
extra.isLike
? const Icon(
size: 20,
CustomIcons.player_dm_tip_like_solid,
color: Colors.white,
)
: const Icon(
size: 20,
CustomIcons.player_dm_tip_like,
color: Colors.white,
),
onTap: () => HeaderControl.likeDanmaku(
extra,
plPlayerController.cid!,
),
),
_dmActionItem(
const Icon(
size: 19,
CustomIcons.player_dm_tip_copy,
color: Colors.white,
),
onTap: () => Utils.copyText(item.content.text),
),
if (item.content.selfSend)
_dmActionItem(
const Icon(
size: 20,
CustomIcons.player_dm_tip_recall,
color: Colors.white,
),
onTap: () => HeaderControl.deleteDanmaku(
extra.id,
plPlayerController.cid!,
),
)
else
_dmActionItem(
const Icon(
size: 20,
CustomIcons.player_dm_tip_back,
color: Colors.white,
),
onTap: () => HeaderControl.reportDanmaku(
context,
extra: extra,
ctr: plPlayerController,
),
),
]
: extra is LiveDanmaku
? [
_dmActionItem(
const Icon(
size: 20,
MdiIcons.accountOutline,
color: Colors.white,
),
onTap: () => Get.toNamed('/member?mid=${extra.mid}'),
),
_dmActionItem(
const Icon(
size: 19,
CustomIcons.player_dm_tip_copy,
color: Colors.white,
),
onTap: () => Utils.copyText(item.content.text),
),
_dmActionItem(
const Icon(
size: 20,
CustomIcons.player_dm_tip_back,
color: Colors.white,
),
onTap: () => HeaderControl.reportLiveDanmaku(
context,
roomId:
(widget.bottomControl
as live_bottom.BottomControl)
.liveRoomCtr
.roomId,
msg: item.content.text,
extra: extra,
ctr: plPlayerController,
),
),
]
: throw UnimplementedError(),
),
),
],
),
),
);