feat: 弹幕设置
This commit is contained in:
@ -29,6 +29,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
bool danmuPlayStatus = true;
|
||||
Box setting = GStrorage.setting;
|
||||
late bool enableShowDanmaku;
|
||||
late List blockTypes;
|
||||
late double showArea;
|
||||
late double opacityVal;
|
||||
late double fontSizeVal;
|
||||
late double danmakuSpeedVal;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,6 +63,11 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
}
|
||||
}
|
||||
});
|
||||
blockTypes = playerController.blockTypes;
|
||||
showArea = playerController.showArea;
|
||||
opacityVal = playerController.opacityVal;
|
||||
fontSizeVal = playerController.fontSizeVal;
|
||||
danmakuSpeedVal = playerController.danmakuSpeedVal;
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
@ -77,6 +87,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
}
|
||||
PlDanmakuController ctr = _plDanmakuController;
|
||||
int currentPosition = position.inMilliseconds;
|
||||
blockTypes = playerController.blockTypes;
|
||||
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
return;
|
||||
@ -99,14 +110,17 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
var delta = currentPosition - element.progress;
|
||||
|
||||
if (delta >= 0 && delta < 200) {
|
||||
_controller!.addItems([
|
||||
DanmakuItem(
|
||||
element.content,
|
||||
color: DmUtils.decimalToColor(element.color),
|
||||
time: element.progress,
|
||||
type: DmUtils.getPosition(element.mode),
|
||||
)
|
||||
]);
|
||||
// 屏蔽彩色弹幕
|
||||
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
|
||||
_controller!.addItems([
|
||||
DanmakuItem(
|
||||
element.content,
|
||||
color: DmUtils.decimalToColor(element.color),
|
||||
time: element.progress,
|
||||
type: DmUtils.getPosition(element.mode),
|
||||
)
|
||||
]);
|
||||
}
|
||||
ctr.currentDmIndex++;
|
||||
} else {
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
@ -135,9 +149,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
||||
widget.playerController.danmakuController = _controller = e;
|
||||
},
|
||||
option: DanmakuOption(
|
||||
fontSize: 15,
|
||||
area: 0.5,
|
||||
duration: 5,
|
||||
fontSize: 15 * fontSizeVal,
|
||||
area: showArea,
|
||||
opacity: opacityVal,
|
||||
duration: danmakuSpeedVal * widget.playerController.playbackSpeed,
|
||||
),
|
||||
statusChanged: (isPlaying) {},
|
||||
),
|
||||
|
@ -17,35 +17,31 @@ class MenuRow extends StatelessWidget {
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: [
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'推荐',
|
||||
selectStatus: true,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'弹幕',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '推荐',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'评论列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '弹幕',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
actionRowLineItem(
|
||||
context,
|
||||
() => {},
|
||||
loadingStatus,
|
||||
'播放列表',
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '评论列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ActionRowLineItem(
|
||||
onTap: () => {},
|
||||
loadingStatus: loadingStatus,
|
||||
text: '播放列表',
|
||||
selectStatus: false,
|
||||
),
|
||||
]),
|
||||
@ -99,3 +95,62 @@ class MenuRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionRowLineItem extends StatelessWidget {
|
||||
final bool? selectStatus;
|
||||
final Function? onTap;
|
||||
final bool? loadingStatus;
|
||||
final String? text;
|
||||
|
||||
const ActionRowLineItem(
|
||||
{super.key,
|
||||
this.selectStatus,
|
||||
this.onTap,
|
||||
this.text,
|
||||
this.loadingStatus = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.secondaryContainer
|
||||
: Colors.transparent,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: InkWell(
|
||||
onTap: () => {
|
||||
feedBack(),
|
||||
onTap!(),
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 4.5),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(30)),
|
||||
border: Border.all(
|
||||
color: selectStatus!
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AnimatedOpacity(
|
||||
opacity: loadingStatus! ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Text(
|
||||
text!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: selectStatus!
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
import 'package:pilipala/models/video/play/url.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
|
||||
final PlPlayerController? controller;
|
||||
@ -29,6 +33,7 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
|
||||
TextStyle titleStyle = const TextStyle(fontSize: 14);
|
||||
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
|
||||
Box localCache = GStrorage.localCache;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -146,9 +151,8 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
// title: Text('播放设置', style: titleStyle),
|
||||
// ),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
onTap: () => {Get.back(), showSetDanmaku()},
|
||||
dense: true,
|
||||
enabled: false,
|
||||
leading: const Icon(Icons.subtitles_outlined, size: 20),
|
||||
title: Text('弹幕设置', style: titleStyle),
|
||||
),
|
||||
@ -454,6 +458,246 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 弹幕功能
|
||||
void showSetDanmaku() async {
|
||||
// 屏蔽类型
|
||||
List<Map<String, dynamic>> blockTypesList = [
|
||||
{'value': 5, 'label': '顶部'},
|
||||
{'value': 2, 'label': '滚动'},
|
||||
{'value': 4, 'label': '底部'},
|
||||
{'value': 6, 'label': '彩色'},
|
||||
];
|
||||
List blockTypes = widget.controller!.blockTypes;
|
||||
// 显示区域
|
||||
List<Map<String, dynamic>> showAreas = [
|
||||
{'value': 0.25, 'label': '1/4屏'},
|
||||
{'value': 0.5, 'label': '半屏'},
|
||||
{'value': 0.75, 'label': '3/4屏'},
|
||||
{'value': 1.0, 'label': '满屏'},
|
||||
];
|
||||
double showArea = widget.controller!.showArea;
|
||||
// 不透明度
|
||||
double opacityVal = widget.controller!.opacityVal;
|
||||
// 字体大小
|
||||
double fontSizeVal = widget.controller!.fontSizeVal;
|
||||
// 弹幕速度
|
||||
double danmakuSpeedVal = widget.controller!.danmakuSpeedVal;
|
||||
|
||||
DanmakuController danmakuController = widget.controller!.danmakuController!;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(builder: (context, StateSetter setState) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 580,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.only(left: 14, right: 14),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 45,
|
||||
child: Center(child: Text('弹幕设置', style: titleStyle)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text('按类型屏蔽'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in blockTypesList) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () async {
|
||||
bool isChoose = blockTypes.contains(i['value']);
|
||||
if (isChoose) {
|
||||
blockTypes.remove(i['value']);
|
||||
} else {
|
||||
blockTypes.add(i['value']);
|
||||
}
|
||||
widget.controller!.blockTypes = blockTypes;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
// 添加或修改其他需要修改的选项属性
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: blockTypes.contains(i['value']),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
const Text('显示区域'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 18),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i in showAreas) ...[
|
||||
ActionRowLineItem(
|
||||
onTap: () {
|
||||
showArea = i['value'];
|
||||
widget.controller!.showArea = showArea;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(area: i['value']);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
text: i['label'],
|
||||
selectStatus: showArea == i['value'],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
Text('不透明度 ${opacityVal * 100}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0,
|
||||
max: 1,
|
||||
value: opacityVal,
|
||||
divisions: 10,
|
||||
label: '${opacityVal * 100}%',
|
||||
onChanged: (double val) {
|
||||
opacityVal = val;
|
||||
widget.controller!.opacityVal = opacityVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(opacity: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 0.5,
|
||||
max: 2.5,
|
||||
value: fontSizeVal,
|
||||
divisions: 20,
|
||||
label: '${(fontSizeVal * 100).toStringAsFixed(1)}%',
|
||||
onChanged: (double val) {
|
||||
fontSizeVal = val;
|
||||
widget.controller!.fontSizeVal = fontSizeVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(
|
||||
fontSize: (15 * fontSizeVal).toDouble(),
|
||||
);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('弹幕时长 ${danmakuSpeedVal.toString()}'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0,
|
||||
bottom: 6,
|
||||
left: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackShape: MSliderTrackShape(),
|
||||
thumbColor: Theme.of(context).colorScheme.primary,
|
||||
activeTrackColor: Theme.of(context).colorScheme.primary,
|
||||
trackHeight: 10,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 6.0),
|
||||
),
|
||||
child: Slider(
|
||||
min: 1,
|
||||
max: 6,
|
||||
value: danmakuSpeedVal,
|
||||
divisions: 10,
|
||||
label: danmakuSpeedVal.toString(),
|
||||
onChanged: (double val) {
|
||||
danmakuSpeedVal = val;
|
||||
widget.controller!.danmakuSpeedVal = danmakuSpeedVal;
|
||||
setState(() {});
|
||||
try {
|
||||
DanmakuOption currentOption =
|
||||
danmakuController.option;
|
||||
DanmakuOption updatedOption =
|
||||
currentOption.copyWith(duration: val);
|
||||
danmakuController.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _ = widget.controller!;
|
||||
@ -556,3 +800,21 @@ class _HeaderControlState extends State<HeaderControl> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
SliderThemeData? sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
const double trackHeight = 3;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2 + 4;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
Box videoStorage = GStrorage.video;
|
||||
Box setting = GStrorage.setting;
|
||||
Box localCache = GStrorage.localCache;
|
||||
|
||||
class PlPlayerController {
|
||||
Player? _videoPlayerController;
|
||||
@ -199,12 +200,30 @@ class PlPlayerController {
|
||||
Rx<bool> isOpenDanmu = false.obs;
|
||||
// 关联弹幕控制器
|
||||
DanmakuController? danmakuController;
|
||||
// 弹幕相关配置
|
||||
late List blockTypes;
|
||||
late double showArea;
|
||||
late double opacityVal;
|
||||
late double fontSizeVal;
|
||||
late double danmakuSpeedVal;
|
||||
|
||||
// 添加一个私有构造函数
|
||||
PlPlayerController._() {
|
||||
_videoType = videoType;
|
||||
isOpenDanmu.value =
|
||||
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||
blockTypes =
|
||||
localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []);
|
||||
showArea = localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5);
|
||||
// 不透明度
|
||||
opacityVal =
|
||||
localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0);
|
||||
// 字体大小
|
||||
fontSizeVal =
|
||||
localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0);
|
||||
// 弹幕速度
|
||||
danmakuSpeedVal =
|
||||
localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0);
|
||||
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
|
||||
// if (status == PlayerStatus.playing) {
|
||||
// WakelockPlus.enable();
|
||||
@ -524,6 +543,12 @@ class PlPlayerController {
|
||||
/// 设置倍速
|
||||
Future<void> setPlaybackSpeed(double speed) async {
|
||||
await _videoPlayerController?.setRate(speed);
|
||||
try {
|
||||
DanmakuOption currentOption = danmakuController!.option;
|
||||
DanmakuOption updatedOption = currentOption.copyWith(
|
||||
duration: (currentOption.duration / speed) * playbackSpeed);
|
||||
danmakuController!.updateOption(updatedOption);
|
||||
} catch (_) {}
|
||||
_playbackSpeed.value = speed;
|
||||
}
|
||||
|
||||
@ -891,6 +916,13 @@ class PlPlayerController {
|
||||
// playerStatus.status.close();
|
||||
// dataStatus.status.close();
|
||||
|
||||
/// 缓存本次弹幕选项
|
||||
localCache.put(LocalCacheKey.danmakuBlockType, blockTypes);
|
||||
localCache.put(LocalCacheKey.danmakuShowArea, showArea);
|
||||
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
|
||||
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
|
||||
localCache.put(LocalCacheKey.danmakuSpeed, danmakuSpeedVal);
|
||||
|
||||
removeListeners();
|
||||
await _videoPlayerController?.dispose();
|
||||
_videoPlayerController = null;
|
||||
|
@ -3,6 +3,7 @@ import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
|
||||
class DmUtils {
|
||||
static Color decimalToColor(int decimalColor) {
|
||||
// 16777215 表示白色
|
||||
int red = (decimalColor >> 16) & 0xFF;
|
||||
int green = (decimalColor >> 8) & 0xFF;
|
||||
int blue = decimalColor & 0xFF;
|
||||
|
@ -34,7 +34,12 @@ class GStrorage {
|
||||
},
|
||||
);
|
||||
// 本地缓存
|
||||
localCache = await Hive.openBox('localCache');
|
||||
localCache = await Hive.openBox(
|
||||
'localCache',
|
||||
compactionStrategy: (entries, deletedEntries) {
|
||||
return deletedEntries > 4;
|
||||
},
|
||||
);
|
||||
// 设置
|
||||
setting = await Hive.openBox('setting');
|
||||
// 搜索历史
|
||||
@ -134,6 +139,13 @@ class LocalCacheKey {
|
||||
//
|
||||
static const String wbiKeys = 'wbiKeys';
|
||||
static const String timeStamp = 'timeStamp';
|
||||
|
||||
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕速度
|
||||
static const String danmakuBlockType = 'danmakuBlockType';
|
||||
static const String danmakuShowArea = 'danmakuShowArea';
|
||||
static const String danmakuOpacity = 'danmakuOpacity';
|
||||
static const String danmakuFontScale = 'danmakuFontScale';
|
||||
static const String danmakuSpeed = 'danmakuSpeed';
|
||||
}
|
||||
|
||||
class VideoBoxKey {
|
||||
|
Reference in New Issue
Block a user