mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-11 10:19:59 +01:00
Control manager and TouchOSC sync
This commit is contained in:
@@ -162,6 +162,21 @@ std::list<std::string> BaseToolkit::splitted(const std::string& str, char delim)
|
||||
return strings;
|
||||
}
|
||||
|
||||
bool BaseToolkit::is_a_number(const std::string& str, int *val)
|
||||
{
|
||||
bool isanumber = false;
|
||||
|
||||
try {
|
||||
*val = std::stoi(str);
|
||||
isanumber = true;
|
||||
}
|
||||
catch (const std::invalid_argument&) {
|
||||
// avoids crash
|
||||
}
|
||||
|
||||
return isanumber;
|
||||
}
|
||||
|
||||
std::string BaseToolkit::common_prefix( const std::list<std::string> & allStrings )
|
||||
{
|
||||
if (allStrings.empty())
|
||||
|
||||
@@ -28,6 +28,9 @@ std::string truncated(const std::string& str, int N);
|
||||
// split a string into list of strings separated by delimitor (e.g. /home/me/toto.mpg -> {home, me, toto.mpg} )
|
||||
std::list<std::string> splitted(const std::string& str, char delim);
|
||||
|
||||
// returns true if the string
|
||||
bool is_a_number(const std::string& str, int *val = nullptr);
|
||||
|
||||
// find common parts in a list of strings
|
||||
std::string common_prefix(const std::list<std::string> &allStrings);
|
||||
std::string common_suffix(const std::list<std::string> &allStrings);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
#define CONTROL_OSC_MSG "OSC: "
|
||||
|
||||
|
||||
void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
const IpEndpointName& remoteEndpoint )
|
||||
{
|
||||
@@ -55,19 +56,30 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
// A wellformed OSC address is in the form '/vimix/target/attribute {arguments}'
|
||||
// First test: should have 3 elements and start with APP_NAME ('vimix')
|
||||
//
|
||||
if (address.size() == 3 && address.front().compare(OSC_PREFIX) == 0 ){
|
||||
if (address.size() > 2 && address.front().compare(OSC_PREFIX) == 0 ){
|
||||
// done with the first part of the OSC address
|
||||
address.pop_front();
|
||||
//
|
||||
// Execute next part of the OSC message according to target
|
||||
//
|
||||
// next part of the OSC message is the target
|
||||
std::string target = address.front();
|
||||
std::string attribute = address.back();
|
||||
// next part of the OSC message is the attribute
|
||||
address.pop_front();
|
||||
std::string attribute = address.front();
|
||||
// Log target: just print text in log window
|
||||
if ( target.compare(OSC_INFO) == 0 )
|
||||
{
|
||||
if ( attribute.compare(OSC_INFO_TEST) == 0)
|
||||
if ( attribute.compare(OSC_INFO_SYNC) == 0) {
|
||||
// send the global status
|
||||
Control::manager().sendStatus(remoteEndpoint);
|
||||
//
|
||||
// send the status of all sources
|
||||
//
|
||||
// (if an argument is given, it indicates the number of sources to update)
|
||||
float N = 0.f;
|
||||
if ( !m.ArgumentStream().Eos())
|
||||
m.ArgumentStream() >> N >> osc::EndMessage;
|
||||
// send the status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, N);
|
||||
}
|
||||
else if ( attribute.compare(OSC_INFO_LOG) == 0)
|
||||
Log::Info(CONTROL_OSC_MSG "received '%s' from %s", m.AddressPattern(), sender);
|
||||
}
|
||||
@@ -97,23 +109,36 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
// Current source target: apply attribute to the current sources
|
||||
else if ( target.compare(OSC_CURRENT) == 0 )
|
||||
{
|
||||
// attributes to change current
|
||||
if ( attribute.compare(OSC_SET) == 0) {
|
||||
int index = 0;
|
||||
m.ArgumentStream() >> index >> osc::EndMessage;
|
||||
Mixer::manager().setCurrentIndex(index);
|
||||
// confirm by sending back the current source attributes
|
||||
Control::manager().sendCurrentSourceAttibutes(remoteEndpoint);
|
||||
}
|
||||
else if ( attribute.compare(OSC_NEXT) == 0) {
|
||||
int sourceid = -1;
|
||||
if ( attribute.compare(OSC_NEXT) == 0) {
|
||||
// set current to NEXT
|
||||
Mixer::manager().setCurrentNext();
|
||||
// confirm by sending back the current source attributes
|
||||
Control::manager().sendCurrentSourceAttibutes(remoteEndpoint);
|
||||
// send the updated status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint);
|
||||
}
|
||||
else if ( attribute.compare(OSC_PREVIOUS) == 0) {
|
||||
// set current to PREVIOUS
|
||||
Mixer::manager().setCurrentPrevious();
|
||||
// confirm by sending back the current source attributes
|
||||
Control::manager().sendCurrentSourceAttibutes(remoteEndpoint);
|
||||
// send the updated status of all sources
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint);
|
||||
}
|
||||
else if ( BaseToolkit::is_a_number( attribute.substr(1), &sourceid) ){
|
||||
// set current to given INDEX
|
||||
Mixer::manager().setCurrentIndex(sourceid);
|
||||
// confirm by sending back the current source attributes
|
||||
Control::manager().sendCurrentSourceAttibutes(remoteEndpoint);
|
||||
//
|
||||
// send the status of all sources
|
||||
//
|
||||
// (if an argument is given, it indicates the number of sources to update)
|
||||
float N = 0.f;
|
||||
if ( !m.ArgumentStream().Eos())
|
||||
m.ArgumentStream() >> N >> osc::EndMessage;
|
||||
Control::manager().sendSourcesStatus(remoteEndpoint, N);
|
||||
}
|
||||
// all other attributes operate on current source
|
||||
else
|
||||
@@ -126,16 +151,10 @@ void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
||||
// try to find source by name
|
||||
Source *s = Mixer::manager().findSource(target);
|
||||
// if failed, try to find source by index
|
||||
if (s == nullptr) {
|
||||
int N = -1;
|
||||
try {
|
||||
N = std::stoi(target);
|
||||
} catch (const std::invalid_argument&) {
|
||||
N = -1;
|
||||
}
|
||||
if (N>=0)
|
||||
s = Mixer::manager().sourceAtIndex(N);
|
||||
}
|
||||
int sourceid = -1;
|
||||
if (s == nullptr && BaseToolkit::is_a_number(target, &sourceid) )
|
||||
s = Mixer::manager().sourceAtIndex(sourceid);
|
||||
// if a source with the given target nameor index was found
|
||||
if (s)
|
||||
Control::manager().receiveSourceAttribute(s, attribute, m.ArgumentStream());
|
||||
else
|
||||
@@ -212,19 +231,19 @@ void Control::receiveOutputAttribute(const std::string &attribute,
|
||||
try {
|
||||
/// e.g. '/vimix/output/enable' or '/vimix/output/enable T' or '/vimix/output/enable F'
|
||||
if ( attribute.compare(OSC_OUTPUT_ENABLE) == 0) {
|
||||
bool on = true;
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
Settings::application.render.disabled = !on;
|
||||
Settings::application.render.disabled = on < 0.5f;
|
||||
}
|
||||
/// e.g. '/vimix/output/disable' or '/vimix/output/disable T' or '/vimix/output/disable F'
|
||||
else if ( attribute.compare(OSC_OUTPUT_DISABLE) == 0) {
|
||||
bool on = true;
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
Settings::application.render.disabled = on;
|
||||
Settings::application.render.disabled = on > 0.5f;
|
||||
}
|
||||
/// e.g. '/vimix/output/fading f 0.2'
|
||||
else if ( attribute.compare(OSC_OUTPUT_FADING) == 0) {
|
||||
@@ -259,19 +278,19 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut
|
||||
try {
|
||||
/// e.g. '/vimix/current/play' or '/vimix/current/play T' or '/vimix/current/play F'
|
||||
if ( attribute.compare(OSC_SOURCE_PLAY) == 0) {
|
||||
bool on = true;
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
target->call( new SetPlay(on) );
|
||||
target->call( new SetPlay(on > 0.5f) );
|
||||
}
|
||||
/// e.g. '/vimix/current/pause' or '/vimix/current/pause T' or '/vimix/current/pause F'
|
||||
else if ( attribute.compare(OSC_SOURCE_PAUSE) == 0) {
|
||||
bool on = true;
|
||||
float on = 1.f;
|
||||
if ( !arguments.Eos()) {
|
||||
arguments >> on >> osc::EndMessage;
|
||||
}
|
||||
target->call( new SetPlay(!on) );
|
||||
target->call( new SetPlay(on < 0.5f) );
|
||||
}
|
||||
/// e.g. '/vimix/current/replay'
|
||||
else if ( attribute.compare(OSC_SOURCE_REPLAY) == 0) {
|
||||
@@ -307,6 +326,16 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut
|
||||
arguments >> x >> y >> osc::EndMessage;
|
||||
target->call( new Resize( x, y), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/turn f 1.0'
|
||||
else if ( attribute.compare(OSC_SOURCE_TURN) == 0) {
|
||||
float x = 0.f;
|
||||
arguments >> x >> osc::EndMessage;
|
||||
target->call( new Turn( x ), true );
|
||||
}
|
||||
/// e.g. '/vimix/current/reset'
|
||||
else if ( attribute.compare(OSC_SOURCE_RESET) == 0) {
|
||||
target->call( new ResetGeometry(), true );
|
||||
}
|
||||
#ifdef CONTROL_DEBUG
|
||||
else {
|
||||
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
||||
@@ -328,49 +357,87 @@ void Control::receiveSourceAttribute(Source *target, const std::string &attribut
|
||||
|
||||
void Control::sendCurrentSourceAttibutes(const IpEndpointName &remoteEndpoint)
|
||||
{
|
||||
// default values
|
||||
char name[21] = {"\0"};
|
||||
float play = 0.f;
|
||||
float depth = 0.f;
|
||||
float alpha = 0.f;
|
||||
|
||||
// fill values if the current source is valid
|
||||
Source *s = Mixer::manager().currentSource();
|
||||
if (s!=nullptr) {
|
||||
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
// create bundle
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
///
|
||||
/// messages
|
||||
///
|
||||
/// Play status
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_PLAY );
|
||||
p << s->playing();
|
||||
p << osc::EndMessage;
|
||||
/// Depth
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_DEPTH );
|
||||
p << s->depth();
|
||||
p << osc::EndMessage;
|
||||
/// Alpha
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_ALPHA );
|
||||
p << s->alpha();
|
||||
p << osc::EndMessage;
|
||||
/// indexed alpha
|
||||
std::string addresspattern = std::string(OSC_PREFIX) + OSC_SEPARATOR;
|
||||
addresspattern += std::to_string(Mixer::manager().indexCurrentSource()) + OSC_SOURCE_ALPHA;
|
||||
p << osc::BeginMessage(addresspattern.c_str());
|
||||
p << s->alpha();
|
||||
p << osc::EndMessage;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
|
||||
// send bundle
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
strncpy(name, s->name().c_str(), 20);
|
||||
play = s->playing() ? 1.f : 0.f;
|
||||
depth = s->depth();
|
||||
alpha = s->alpha();
|
||||
}
|
||||
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
// create bundle
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
/// name
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_NAME ) << name << osc::EndMessage;
|
||||
/// Play status
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_PLAY ) << play << osc::EndMessage;
|
||||
/// Depth
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_DEPTH ) << depth << osc::EndMessage;
|
||||
/// Alpha
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_CURRENT OSC_SOURCE_ALPHA ) << alpha << osc::EndMessage;
|
||||
|
||||
// send bundle
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
}
|
||||
|
||||
|
||||
void Control::sendSourcesStatus(const IpEndpointName &remoteEndpoint, float max_count)
|
||||
{
|
||||
// build socket to send message to indicated endpoint
|
||||
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
||||
|
||||
// build messages packet
|
||||
char buffer[IP_MTU_SIZE];
|
||||
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
||||
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
int i = 0;
|
||||
char oscaddr[128];
|
||||
int index_current = Mixer::manager().indexCurrentSource();
|
||||
for (; i < Mixer::manager().count(); ++i) {
|
||||
// send status of currently selected
|
||||
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
||||
p << osc::BeginMessage( oscaddr ) << (index_current == i ? 1.f : 0.f) << osc::EndMessage;
|
||||
|
||||
// send status of alpha
|
||||
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
||||
p << osc::BeginMessage( oscaddr ) << Mixer::manager().sourceAtIndex(i)->alpha() << osc::EndMessage;
|
||||
}
|
||||
|
||||
for (; i < max_count; ++i) {
|
||||
// reset status of currently selected
|
||||
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
||||
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
||||
|
||||
// reset status of alpha
|
||||
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
||||
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
||||
}
|
||||
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
}
|
||||
|
||||
|
||||
void Control::sendStatus(const IpEndpointName &remoteEndpoint)
|
||||
{
|
||||
// build socket to send message to indicated endpoint
|
||||
@@ -383,28 +450,21 @@ void Control::sendStatus(const IpEndpointName &remoteEndpoint)
|
||||
p.Clear();
|
||||
p << osc::BeginBundle();
|
||||
|
||||
///
|
||||
/// messages
|
||||
///
|
||||
/// Agree to test
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_INFO OSC_INFO_TEST );
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_INFO OSC_INFO_SYNC );
|
||||
p << true;
|
||||
p << osc::EndMessage;
|
||||
|
||||
/// send output attributes
|
||||
/// output attributes
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_ENABLE );
|
||||
p << !Settings::application.render.disabled;
|
||||
p << (Settings::application.render.disabled ? 0.f : 1.f);
|
||||
p << osc::EndMessage;
|
||||
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_FADING );
|
||||
p << Mixer::manager().session()->fading();
|
||||
p << osc::EndMessage;
|
||||
|
||||
/// send all sources alpha
|
||||
for (int i = 0; i < Mixer::manager().count(); ++i) {
|
||||
std::string addresspattern = std::string(OSC_PREFIX) + OSC_SEPARATOR;
|
||||
addresspattern += std::to_string(i) + OSC_SOURCE_ALPHA;
|
||||
p << osc::BeginMessage(addresspattern.c_str());
|
||||
p << Mixer::manager().sourceAtIndex(i)->alpha();
|
||||
p << osc::EndMessage;
|
||||
}
|
||||
|
||||
p << osc::EndBundle;
|
||||
socket.Send( p.Data(), p.Size() );
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "NetworkToolkit.h"
|
||||
|
||||
#define OSC_INFO "/info"
|
||||
#define OSC_INFO_TEST "/test"
|
||||
#define OSC_INFO_SYNC "/sync"
|
||||
#define OSC_INFO_LOG "/log"
|
||||
|
||||
#define OSC_OUTPUT "/output"
|
||||
@@ -17,9 +17,8 @@
|
||||
#define OSC_CURRENT "/current"
|
||||
#define OSC_NEXT "/next"
|
||||
#define OSC_PREVIOUS "/previous"
|
||||
#define OSC_SET "/set"
|
||||
//#define OSC_VERSION "version"
|
||||
|
||||
#define OSC_SOURCE_NAME "/name"
|
||||
#define OSC_SOURCE_PLAY "/play"
|
||||
#define OSC_SOURCE_PAUSE "/pause"
|
||||
#define OSC_SOURCE_REPLAY "/replay"
|
||||
@@ -28,6 +27,8 @@
|
||||
#define OSC_SOURCE_DEPTH "/depth"
|
||||
#define OSC_SOURCE_GRAB "/grab"
|
||||
#define OSC_SOURCE_RESIZE "/resize"
|
||||
#define OSC_SOURCE_TURN "/turn"
|
||||
#define OSC_SOURCE_RESET "/reset"
|
||||
|
||||
class Session;
|
||||
class Source;
|
||||
@@ -69,6 +70,7 @@ protected:
|
||||
osc::ReceivedMessageArgumentStream arguments);
|
||||
|
||||
void sendCurrentSourceAttibutes(const IpEndpointName& remoteEndpoint);
|
||||
void sendSourcesStatus(const IpEndpointName& remoteEndpoint, float max_count = 0.f);
|
||||
void sendStatus(const IpEndpointName& remoteEndpoint);
|
||||
|
||||
private:
|
||||
|
||||
@@ -27,6 +27,16 @@ SourceCallback::SourceCallback(): active_(true), finished_(false), initialized_(
|
||||
{
|
||||
}
|
||||
|
||||
void ResetGeometry::update(Source *s, float)
|
||||
{
|
||||
s->group(View::GEOMETRY)->scale_ = glm::vec3(1.f);
|
||||
s->group(View::GEOMETRY)->rotation_.z = 0;
|
||||
s->group(View::GEOMETRY)->crop_ = glm::vec3(1.f);
|
||||
s->group(View::GEOMETRY)->translation_ = glm::vec3(0.f);
|
||||
s->touch();
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
SetAlpha::SetAlpha(float alpha) : SourceCallback(), alpha_(CLAMP(alpha, 0.f, 1.f))
|
||||
{
|
||||
step_ = glm::normalize(glm::vec2(1.f, 1.f)); // step in diagonal by default
|
||||
@@ -196,3 +206,36 @@ void Resize::update(Source *s, float dt)
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
Turn::Turn(float da, float duration) : SourceCallback(), speed_(da),
|
||||
duration_(duration), progress_(0.f)
|
||||
{
|
||||
}
|
||||
|
||||
void Turn::update(Source *s, float dt)
|
||||
{
|
||||
if (s && !s->locked()) {
|
||||
// reset on first run or upon call of reset()
|
||||
if (!initialized_){
|
||||
// start animation
|
||||
progress_ = 0.f;
|
||||
// initial position
|
||||
start_ = s->group(View::GEOMETRY)->rotation_.z;
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// calculate amplitude of movement
|
||||
progress_ += dt;
|
||||
|
||||
// perform movement
|
||||
s->group(View::GEOMETRY)->rotation_.z = start_ + speed_ * ( dt * -0.001f / M_PI);
|
||||
|
||||
// timeout
|
||||
if ( progress_ > duration_ ) {
|
||||
// done
|
||||
finished_ = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
finished_ = true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ public:
|
||||
CALLBACK_PLAY,
|
||||
CALLBACK_REPLAY,
|
||||
CALLBACK_GRAB,
|
||||
CALLBACK_RESIZE
|
||||
CALLBACK_RESIZE,
|
||||
CALLBACK_TURN
|
||||
} CallbackType;
|
||||
|
||||
SourceCallback();
|
||||
@@ -35,6 +36,14 @@ protected:
|
||||
bool initialized_;
|
||||
};
|
||||
|
||||
class ResetGeometry : public SourceCallback
|
||||
{
|
||||
|
||||
public:
|
||||
ResetGeometry() : SourceCallback() {}
|
||||
void update(Source *s, float) override;
|
||||
};
|
||||
|
||||
class SetAlpha : public SourceCallback
|
||||
{
|
||||
float alpha_;
|
||||
@@ -107,5 +116,19 @@ public:
|
||||
CallbackType type () override { return CALLBACK_RESIZE; }
|
||||
};
|
||||
|
||||
class Turn : public SourceCallback
|
||||
{
|
||||
float speed_;
|
||||
float start_;
|
||||
float target_;
|
||||
float duration_;
|
||||
float progress_;
|
||||
|
||||
public:
|
||||
Turn(float da, float duration = 0.f);
|
||||
void update(Source *s, float) override;
|
||||
CallbackType type () override { return CALLBACK_TURN; }
|
||||
};
|
||||
|
||||
|
||||
#endif // SOURCECALLBACK_H
|
||||
|
||||
BIN
rsc/osc/vimix.mk1.touchosc
Normal file
BIN
rsc/osc/vimix.mk1.touchosc
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user