mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
live dm action
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -260,7 +260,7 @@ class ReportOptions {
|
||||
4: '辱骂引战',
|
||||
5: '政治敏感',
|
||||
6: '青少年不良信息',
|
||||
7: '其他 ', // avoid show form
|
||||
7: '其他', // avoid show form
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user