mirror of
https://github.com/hyperion-project/hyperion.ng.git
synced 2023-10-10 13:36:59 +02:00
Webui: view led configuration + lights (#223)
* make hyperion websocket api event based * implement new websocket handling for generalconf * migrate all webui stuff to new event based websocket api some cleanup ... now all html templates are in content refactoring of web stuff * add hyperionport to global start impl. removing advanced key * separate dashboard serverinfo is updated every 3 seconds automatily add input selection cleanup and remove not needed stuff * prepare infrastructure for server sided file execution * webui minor fixes * fix compile * implement led layout view with live colors * live led vies * fix general conf unrigister ledcolors request, when not on leds.html * fix compiler warning * prepare realtime ledview and enhance ui
This commit is contained in:
parent
b99e75d7e4
commit
bbffa078fd
@ -1,7 +1,7 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header" lang="en" data-lang-token="main_menu_effects_token">Effects</h1>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header" lang="en" data-lang-token="main_menu_effects_token">Effects</h1>
|
||||
<div class="introd">
|
||||
<h4 lang="en" data-lang-token="remote_effects_intro">The Effects remote enables you to set an effect for testing or demonstration purposes. Don't forget to stop it afterwards.</h4>
|
||||
</div>
|
||||
|
@ -17,7 +17,8 @@
|
||||
|
||||
|
||||
<script>
|
||||
function removeAdvanced(obj)
|
||||
/*
|
||||
function removeAdvanced(obj,searchStack)
|
||||
{
|
||||
searchStack = [];
|
||||
$.each(obj, function(key, val) {
|
||||
@ -35,33 +36,33 @@
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
*/
|
||||
$(hyperion).on("cmd-config-getschema", function(event) {
|
||||
parsedConfSchemaJSON = event.response.result;
|
||||
|
||||
// remove all "advanced" options from schema
|
||||
//removeAdvanced(parsedConfSchemaJSON); // not working atm
|
||||
//removeAdvanced(parsedConfSchemaJSON, []); // not working atm
|
||||
//console.log(JSON.stringify(parsedConfSchemaJSON));
|
||||
|
||||
schema_smoothing = parsedConfSchemaJSON.properties.smoothing;
|
||||
schema_kodiVideoChecker = parsedConfSchemaJSON.properties.kodiVideoChecker;
|
||||
schema_initialEffect = parsedConfSchemaJSON.properties.initialEffect;
|
||||
//schema_grabber-v4l2 = parsedConfSchemaJSON.properties.grabber-v4l2;
|
||||
schema_framegrabber = parsedConfSchemaJSON.properties.framegrabber;
|
||||
schema_forwarder = parsedConfSchemaJSON.properties.forwarder;
|
||||
schema_effects = parsedConfSchemaJSON.properties.effects;
|
||||
schema_device = parsedConfSchemaJSON.properties.device;
|
||||
schema_color = parsedConfSchemaJSON.properties.color;
|
||||
schema_blackborderdetector = parsedConfSchemaJSON.properties.blackborderdetector;
|
||||
schema_logger = parsedConfSchemaJSON.properties.logger;
|
||||
schema_jsonServer = parsedConfSchemaJSON.properties.jsonServer;
|
||||
schema_protoServer = parsedConfSchemaJSON.properties.protoServer;
|
||||
schema_boblightServer = parsedConfSchemaJSON.properties.boblightServer;
|
||||
schema_udpListener = parsedConfSchemaJSON.properties.udpListener;
|
||||
schema_webConfig = parsedConfSchemaJSON.properties.webConfig;
|
||||
schema = parsedConfSchemaJSON.properties;
|
||||
schema_blackborderdetector = schema.blackborderdetector;
|
||||
schema_color = schema.color,
|
||||
schema_device = schema.device,
|
||||
schema_effects = schema.effects,
|
||||
schema_forwarder = schema.forwarder,
|
||||
schema_framegrabber = schema.framegrabber,
|
||||
//schema_grabber-v4l2 = schema.grabber-v4l2,
|
||||
schema_initialEffect = schema.initialEffect,
|
||||
schema_kodiVideoChecker = schema.kodiVideoChecker,
|
||||
schema_smoothing = schema.smoothing,
|
||||
schema_logger = schema.logger,
|
||||
schema_jsonServer = schema.jsonServer,
|
||||
schema_protoServer = schema.protoServer,
|
||||
schema_boblightServer = schema.boblightServer,
|
||||
schema_udpListener = schema.udpListener,
|
||||
schema_webConfig = schema.webConfig
|
||||
|
||||
var element = document.getElementById('editor_holder');
|
||||
//JSONEditor.defaults.options.theme = 'bootstrap3';
|
||||
|
||||
var general_conf_editor = new JSONEditor(element,{
|
||||
theme: 'bootstrap3',
|
||||
disable_collapse: 'true',
|
||||
@ -98,7 +99,7 @@
|
||||
|
||||
document.getElementById('submit').addEventListener('click',function() {
|
||||
// Get the value from the editor
|
||||
console.log(general_conf_editor.getValue());
|
||||
//console.log(general_conf_editor.getValue());
|
||||
});
|
||||
// $("[type='checkbox']").bootstrapSwitch();
|
||||
});
|
||||
|
@ -1,6 +1,15 @@
|
||||
<div style="text-align:center;margin:auto;padding:auto">
|
||||
<div id="hyperion_inputs" />
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="page-header" lang="en" data-lang-token="main_menu_input_selection_token">Input Selection</h1>
|
||||
<!-- <div class="introd">
|
||||
<h4 lang="en" data-lang-token="remote_effects_intro"></h4>
|
||||
</div>-->
|
||||
|
||||
<div style="text-align:center;margin:auto;padding:auto">
|
||||
<div id="hyperion_inputs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
$(hyperion).on("cmd-serverinfo", function(event) {
|
||||
@ -22,10 +31,8 @@
|
||||
|
||||
var max_width=200;
|
||||
$('.btn_input_selection').each(function() {
|
||||
elem_width = $(this).css('width')
|
||||
elem_width = elem_width.substring(0, elem_width.length - 2);
|
||||
if (elem_width > max_width)
|
||||
max_width = elem_width;
|
||||
if ($(this).innerWidth() > max_width)
|
||||
max_width = $(this).innerWidth();
|
||||
});
|
||||
$('.btn_input_selection').css("min-width",max_width+"px");
|
||||
});
|
||||
|
125
assets/webconfig/content/leds.html
Normal file
125
assets/webconfig/content/leds.html
Normal file
@ -0,0 +1,125 @@
|
||||
<style>
|
||||
#leds_canvas {width:800px; height:450px; background-color:#AAAAAA; position:absolute; margin:10px; box-shadow: 10px 10px 5px #BBBBBB;
|
||||
background-image:url(/img/hyperion/hyperionlogo.png); background-repeat:no-repeat; background-position: center; border-radius:2px; }
|
||||
.led { display:inline-block; border: 1px solid black; position:absolute; opacity:0.8; text-align:center;
|
||||
vertical-align:middle; padding:4px; border-radius:2px;}
|
||||
.led_num {display:none; position:relative; color:black; background-color: white;
|
||||
border-radius:2px; padding:1px; vertical-align:middle; text-align:center; font-size:0.8em;}
|
||||
#leds_controls {white-space:nowrap; margin-top:530px;}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="page-header" lang="en" data-lang-token="main_menu_leds_conf_token">Led Configuration</h1>
|
||||
<!-- <div class="introd">
|
||||
<h4 lang="en" data-lang-token="remote_effects_intro"></h4>
|
||||
</div>-->
|
||||
|
||||
<div id="leds_canvas"/>
|
||||
|
||||
<div id="leds_controls">
|
||||
<button lang="en" type="button" class="btn btn-success" id="leds_toggle">toggle leds</button>
|
||||
<button lang="en" type="button" class="btn btn-danger" id="leds_toggle_num">toggle led numbers</button>
|
||||
<button lang="en" type="button" class="btn btn-success" id="leds_toggle_live">toggle live leds</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function updateLedColors()
|
||||
{
|
||||
if($("#leds_canvas").length > 0 && ledStreamActive)
|
||||
{
|
||||
requestLedColorsStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
ledStreamActivate(false);
|
||||
}
|
||||
}
|
||||
|
||||
function ledStreamActivate(enable)
|
||||
{
|
||||
$(hyperion).off("cron", updateLedColors );
|
||||
if ( enable && ! ledStreamActive )
|
||||
{
|
||||
$(hyperion).on("cron", updateLedColors );
|
||||
}
|
||||
|
||||
ledStreamActive=enable;
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
$(hyperion).on("cmd-ledcolors",function(event){
|
||||
ledColors = (event.response.result);
|
||||
for(var idx=0; idx<ledColors.length; idx++)
|
||||
{
|
||||
led = ledColors[idx]
|
||||
$("#led_"+led.index).css("background","rgb("+led.red+","+led.green+","+led.blue+")");
|
||||
}
|
||||
});
|
||||
|
||||
$(hyperion).on("cmd-ledcolors-ledstream-update",function(event){
|
||||
ledColors = (event.response.result);
|
||||
for(var idx=0; idx<ledColors.length; idx++)
|
||||
{
|
||||
led = ledColors[idx]
|
||||
$("#led_"+led.index).css("background","rgb("+led.red+","+led.green+","+led.blue+")");
|
||||
}
|
||||
});
|
||||
|
||||
$(hyperion).on("cmd-config-getconfig",function(event){
|
||||
parsedConfJSON = event.response.result;
|
||||
leds = parsedConfJSON.leds;
|
||||
canvas_height = $('#leds_canvas').innerHeight();
|
||||
canvas_width = $('#leds_canvas').innerWidth();
|
||||
|
||||
leds_html = "";
|
||||
for(var idx=0; idx<leds.length; idx++)
|
||||
{
|
||||
led = leds[idx];
|
||||
led_id='led_'+led.index;
|
||||
bgcolor = "background-color:hsl("+(idx*360/leds.length)+",100%,50%);";
|
||||
pos = "left:"+(led.hscan.minimum * canvas_width)+"px;"+
|
||||
"top:"+(led.vscan.minimum * canvas_height)+"px;"+
|
||||
"width:"+((led.hscan.maximum-led.hscan.minimum) * canvas_width-1)+"px;"+
|
||||
"height:"+((led.vscan.maximum-led.vscan.minimum) * canvas_height-1)+"px;";
|
||||
leds_html += '<div id="'+led_id+'" class="led" style="'+bgcolor+pos+'" title="'+led.index+'"><span id="'+led_id+'_num" class="led_num">'+led.index+'</span></div>';
|
||||
}
|
||||
$('#leds_canvas').html(leds_html);
|
||||
$('#led_0').css("border","2px dotted red");
|
||||
ledStreamActivate(true)
|
||||
});
|
||||
|
||||
$('#leds_toggle_num').on("click", function() {
|
||||
$('.led_num').toggle();
|
||||
toggleClass('#leds_toggle_num', "btn-danger", "btn-success");
|
||||
});
|
||||
|
||||
$('#leds_toggle').on("click", function() {
|
||||
$('.led').toggle();
|
||||
toggleClass('#leds_toggle', "btn-success", "btn-danger");
|
||||
});
|
||||
|
||||
$('#leds_toggle_live').on("click", function() {
|
||||
setClassByBool('#leds_toggle_live',ledStreamActive,"btn-success","btn-danger");
|
||||
if ( ledStreamActive )
|
||||
{
|
||||
ledStreamActivate(false);
|
||||
|
||||
led_count = $(".led").length;
|
||||
for(var idx=0; idx<led_count; idx++)
|
||||
{
|
||||
$('#led_'+idx).css("background-color","hsl("+(idx*360/led_count)+",100%,50%)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ledStreamActivate(true);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
requestServerConfig();
|
||||
});
|
||||
</script>
|
@ -202,6 +202,7 @@
|
||||
<ul class="nav nav-second-level">
|
||||
<li> <a class="inactive" id="load_confGeneral"><i class="fa fa-play-circle-o fa-fw"></i><span lang="en" data-lang-token="main_menu_general_conf_token">General</span></a> </li>
|
||||
<li> <a class="inactive" id="load_confKodi"><i class="fa fa-play-circle-o fa-fw"></i><span lang="en" data-lang-token="main_menu_kodiwatch_token">Kodi Watch</span></a> </li>
|
||||
<li> <a class="inactive" id="load_confLeds"><i class="fa fa-play-circle-o fa-fw"></i><span lang="en" data-lang-token="main_menu_leds_conf_token">LEDs</span></a> </li>
|
||||
<li> <a class="inactive" id="load_huebridge"><i class="fa fa-cog fa-fw"></i><span lang="en" data-lang-token="main_menu_huebridge_token">Hue Bridge</span></a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -261,15 +262,16 @@
|
||||
$(document).ready( function() {
|
||||
initWebSocket();
|
||||
bindNavToContent("#load_dashboard","dashboard",true);
|
||||
bindNavToContent("#load_lighttest","lighttest");
|
||||
bindNavToContent("#load_effects","effects");
|
||||
bindNavToContent("#load_components","remote_components");
|
||||
bindNavToContent("#load_input_selection","input_selection");
|
||||
bindNavToContent("#load_huebridge","huebridge");
|
||||
bindNavToContent("#load_support","support");
|
||||
bindNavToContent("#load_confKodi","kodiconf");
|
||||
bindNavToContent("#load_update","update");
|
||||
bindNavToContent("#load_confGeneral","generalconf");
|
||||
bindNavToContent("#load_lighttest","lighttest",false);
|
||||
bindNavToContent("#load_effects","effects",false);
|
||||
bindNavToContent("#load_components","remote_components",false);
|
||||
bindNavToContent("#load_input_selection","input_selection",false);
|
||||
bindNavToContent("#load_huebridge","huebridge",false);
|
||||
bindNavToContent("#load_support","support",false);
|
||||
bindNavToContent("#load_confKodi","kodiconf",false);
|
||||
bindNavToContent("#load_update","update",false);
|
||||
bindNavToContent("#load_confGeneral","generalconf",false);
|
||||
bindNavToContent("#load_confLeds","leds",false);
|
||||
|
||||
//Change all Checkboxes to Switches
|
||||
$("[type='checkbox']").bootstrapSwitch();
|
||||
@ -313,7 +315,6 @@
|
||||
$('#error_dialog .modal-body').html(event.reason);
|
||||
$('#error_dialog').modal('show');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$(function(){
|
||||
|
@ -26,19 +26,23 @@ var currentVersion;
|
||||
var cleanCurrentVersion;
|
||||
var latestVersion;
|
||||
var cleanLatestVersion;
|
||||
var parsedServerInfoJSON;
|
||||
var parsedUpdateJSON;
|
||||
var parsedConfSchemaJSON;
|
||||
var parsedServerInfoJSON = {};
|
||||
var parsedUpdateJSON = {};
|
||||
var parsedConfSchemaJSON = {};
|
||||
var parsedConfJSON = {};
|
||||
var hyperionport = 19444;
|
||||
var websocket = null;
|
||||
var hyperion = {};
|
||||
var wsTan = 1;
|
||||
var cronId = 0;
|
||||
var ledStreamActive=false;
|
||||
|
||||
|
||||
//
|
||||
function cron()
|
||||
{
|
||||
requestServerInfo();
|
||||
$(hyperion).trigger({type:"cron"});
|
||||
}
|
||||
|
||||
// init websocket to hyperion and bind socket events to jquery events of $(hyperion) object
|
||||
@ -54,7 +58,7 @@ function initWebSocket()
|
||||
|
||||
websocket.onopen = function (event) {
|
||||
$(hyperion).trigger({type:"open"});
|
||||
cronId = window.setInterval(cron,3000);
|
||||
cronId = window.setInterval(cron,2000);
|
||||
};
|
||||
|
||||
websocket.onclose = function (event) {
|
||||
@ -130,6 +134,14 @@ function requestServerConfigSchema() {
|
||||
websocket.send('{"command":"config", "tan":'+wsTan+',"subcommand":"getschema"}');
|
||||
}
|
||||
|
||||
function requestServerConfig() {
|
||||
websocket.send('{"command":"config", "tan":'+wsTan+',"subcommand":"getconfig"}');
|
||||
}
|
||||
|
||||
function requestLedColorsStart() {
|
||||
websocket.send('{"command":"ledcolors", "tan":'+wsTan+',"subcommand":"ledstream_start"}');
|
||||
}
|
||||
|
||||
function requestPriorityClear() {
|
||||
websocket.send('{"command":"clear", "tan":'+wsTan+', "priority":1}');
|
||||
}
|
||||
@ -142,7 +154,6 @@ function requestSetColor(r,g,b) {
|
||||
websocket.send('{"command":"color", "tan":'+wsTan+', "color":['+r+','+g+','+b+'], "priority":1}');
|
||||
}
|
||||
|
||||
|
||||
function requestSetComponentState(comp, state){
|
||||
state_str = state?"true":"false";
|
||||
websocket.send('{"command":"componentstate", "tan":'+wsTan+',"componentstate":{"component":"'+comp+'","state":'+state_str+'}}');
|
||||
|
@ -10,3 +10,34 @@ function bindNavToContent(containerId, fileName, loadNow)
|
||||
$("#page-wrapper").load("/content/"+fileName+".html");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toggleClass(obj,class1,class2)
|
||||
{
|
||||
if ( $(obj).hasClass(class1))
|
||||
{
|
||||
$(obj).removeClass(class1);
|
||||
$(obj).addClass(class2);
|
||||
}
|
||||
else
|
||||
{
|
||||
$(obj).removeClass(class2);
|
||||
$(obj).addClass(class1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setClassByBool(obj,enable,class1,class2)
|
||||
{
|
||||
if (enable)
|
||||
{
|
||||
$(obj).removeClass(class1);
|
||||
$(obj).addClass(class2);
|
||||
}
|
||||
else
|
||||
{
|
||||
$(obj).removeClass(class2);
|
||||
$(obj).addClass(class1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,7 @@ public:
|
||||
/// @return The current priority
|
||||
///
|
||||
int getCurrentPriority() const;
|
||||
|
||||
///
|
||||
/// Returns a list of active priorities
|
||||
///
|
||||
|
@ -273,6 +273,8 @@ void JsonClientConnection::handleMessage(const std::string &messageString)
|
||||
handleConfigCommand(message, command, tan);
|
||||
else if (command == "componentstate")
|
||||
handleComponentStateCommand(message, command, tan);
|
||||
else if (command == "ledcolors")
|
||||
handleLedColorsCommand(message, command, tan);
|
||||
else
|
||||
handleNotImplemented();
|
||||
}
|
||||
@ -975,6 +977,31 @@ void JsonClientConnection::handleComponentStateCommand(const Json::Value& messag
|
||||
}
|
||||
}
|
||||
|
||||
void JsonClientConnection::handleLedColorsCommand(const Json::Value&, const std::string &command, const int tan)
|
||||
{
|
||||
// create result
|
||||
Json::Value result;
|
||||
result["success"] = true;
|
||||
result["command"] = command;
|
||||
result["tan"] = tan;
|
||||
Json::Value & leds = result["result"] = Json::Value(Json::arrayValue);
|
||||
|
||||
const PriorityMuxer::InputInfo & priorityInfo = _hyperion->getPriorityInfo(_hyperion->getCurrentPriority());
|
||||
std::vector<ColorRgb> ledBuffer = priorityInfo.ledColors;
|
||||
|
||||
for (ColorRgb& color : ledBuffer)
|
||||
{
|
||||
int idx = leds.size();
|
||||
Json::Value & item = leds[idx];
|
||||
item["index"] = idx;
|
||||
item["red"] = color.red;
|
||||
item["green"] = color.green;
|
||||
item["blue"] = color.blue;
|
||||
}
|
||||
// send the result
|
||||
sendMessage(result);
|
||||
}
|
||||
|
||||
void JsonClientConnection::handleNotImplemented()
|
||||
{
|
||||
sendErrorReply("Command not implemented");
|
||||
|
@ -169,6 +169,12 @@ private:
|
||||
///
|
||||
void handleComponentStateCommand(const Json::Value & message, const std::string &command, const int tan);
|
||||
|
||||
/// Handle an incoming JSON Led Colors message
|
||||
///
|
||||
/// @param message the incoming message
|
||||
///
|
||||
void handleLedColorsCommand(const Json::Value &, const std::string &command, const int tan);
|
||||
|
||||
///
|
||||
/// Handle an incoming JSON message of unknown type
|
||||
///
|
||||
@ -222,7 +228,6 @@ private:
|
||||
///
|
||||
bool checkJson(const Json::Value & message, const QString &schemaResource, std::string & errors, bool ignoreRequired = false);
|
||||
|
||||
private:
|
||||
/// The TCP-Socket that is connected tot the Json-client
|
||||
QTcpSocket * _socket;
|
||||
|
||||
@ -243,4 +248,8 @@ private:
|
||||
|
||||
/// Flag if forwarder is enabled
|
||||
bool _forwarder_enabled;
|
||||
|
||||
///
|
||||
QTimer _timer_ledcolors;
|
||||
|
||||
};
|
||||
|
@ -14,5 +14,6 @@
|
||||
<file alias="schema-sourceselect">schema/schema-sourceselect.json</file>
|
||||
<file alias="schema-config">schema/schema-config.json</file>
|
||||
<file alias="schema-componentstate">schema/schema-componentstate.json</file>
|
||||
<file alias="schema-ledcolors">schema/schema-ledcolors.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
27
libsrc/jsonserver/schema/schema-ledcolors.json
Normal file
27
libsrc/jsonserver/schema/schema-ledcolors.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"type":"object",
|
||||
"required":true,
|
||||
"properties":{
|
||||
"command": {
|
||||
"type" : "string",
|
||||
"required" : true,
|
||||
"enum" : ["ledcolors"]
|
||||
},
|
||||
"tan" : {
|
||||
"type" : "integer"
|
||||
},
|
||||
"subcommand": {
|
||||
"type" : "string",
|
||||
"required" : true,
|
||||
"enum" : ["ledstream_stop","ledstream_start","testled","imagestream_start","imagestream_stop"]
|
||||
},
|
||||
"oneshot": {
|
||||
"type" : "bool"
|
||||
},
|
||||
"interval": {
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
|
||||
"additionalProperties": false
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
"command": {
|
||||
"type" : "string",
|
||||
"required" : true,
|
||||
"enum" : ["color", "image", "effect", "serverinfo", "clear", "clearall", "transform", "correction", "temperature", "adjustment", "sourceselect", "config", "componentstate"]
|
||||
"enum" : ["color", "image", "effect", "serverinfo", "clear", "clearall", "transform", "correction", "temperature", "adjustment", "sourceselect", "config", "componentstate", "ledcolors"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,14 +94,13 @@ int main(int argc, char** argv)
|
||||
{
|
||||
if (loadConfig(configFile))
|
||||
std::cout << "PASSED" << std::endl;
|
||||
exit(0);
|
||||
return 0;
|
||||
}
|
||||
catch (std::runtime_error exception)
|
||||
{
|
||||
std::cout << "FAILED" << std::endl;
|
||||
std::cout << exception.what() << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user