mirror of
https://github.com/clementine-player/Clementine
synced 2025-02-01 20:06:53 +01:00
Merge branch 'master' of https://code.google.com/r/asfa194-clementineremote
This commit is contained in:
commit
53608665e4
@ -5,13 +5,14 @@ enum MsgType {
|
|||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
// Messages generally send from client to server
|
// Messages generally send from client to server
|
||||||
CONNECT = 1;
|
CONNECT = 1;
|
||||||
DISCONNECT = 2;
|
|
||||||
REQUEST_PLAYLISTS = 3;
|
REQUEST_PLAYLISTS = 3;
|
||||||
REQUEST_PLAYLIST_SONGS = 4;
|
REQUEST_PLAYLIST_SONGS = 4;
|
||||||
CHANGE_SONG = 5;
|
CHANGE_SONG = 5;
|
||||||
SET_VOLUME = 6;
|
SET_VOLUME = 6;
|
||||||
|
SET_TRACK_POSITION = 7;
|
||||||
|
|
||||||
// Messages send by both
|
// Messages send by both
|
||||||
|
DISCONNECT = 2;
|
||||||
PLAY = 20;
|
PLAY = 20;
|
||||||
PLAYPAUSE = 21;
|
PLAYPAUSE = 21;
|
||||||
PAUSE = 22;
|
PAUSE = 22;
|
||||||
@ -146,20 +147,45 @@ message ResponseUpdateTrackPosition {
|
|||||||
optional int32 position = 1;
|
optional int32 position = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The connect message containing the authentication code
|
||||||
|
message RequestConnect {
|
||||||
|
optional int32 auth_code = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respone, why the connection was closed
|
||||||
|
enum ReasonDisconnect {
|
||||||
|
Server_Shutdown = 1;
|
||||||
|
Wrong_Auth_Code = 2;
|
||||||
|
}
|
||||||
|
message ResponseDisconnect {
|
||||||
|
optional ReasonDisconnect reason_disconnect = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A client requests a new track position
|
||||||
|
// position in seconds!
|
||||||
|
message RequestSetTrackPosition {
|
||||||
|
optional int32 position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// The message itself
|
// The message itself
|
||||||
message Message {
|
message Message {
|
||||||
optional int32 version = 1 [default=2];
|
optional int32 version = 1 [default=2];
|
||||||
optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message?
|
optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message?
|
||||||
|
|
||||||
|
optional RequestConnect request_connect = 21;
|
||||||
optional RequestPlaylistSongs request_playlist_songs = 10;
|
optional RequestPlaylistSongs request_playlist_songs = 10;
|
||||||
optional RequestChangeSong request_change_song = 11;
|
optional RequestChangeSong request_change_song = 11;
|
||||||
optional RequestSetVolume request_set_volume = 12;
|
optional RequestSetVolume request_set_volume = 12;
|
||||||
|
optional RequestSetTrackPosition request_set_track_position = 23;
|
||||||
|
|
||||||
optional Repeat repeat = 13;
|
optional Repeat repeat = 13;
|
||||||
optional Shuffle shuffle = 14;
|
optional Shuffle shuffle = 14;
|
||||||
|
|
||||||
optional ResponseClementineInfo response_clementine_info = 15;
|
optional ResponseClementineInfo response_clementine_info = 15;
|
||||||
optional ResponseCurrentMetadata response_current_metadata = 16;
|
optional ResponseCurrentMetadata response_current_metadata = 16;
|
||||||
optional ResponsePlaylists response_playlists = 17;
|
optional ResponsePlaylists response_playlists = 17;
|
||||||
optional ResponsePlaylistSongs response_playlist_songs = 18;
|
optional ResponsePlaylistSongs response_playlist_songs = 18;
|
||||||
optional ResponseEngineStateChanged response_engine_state_changed = 19;
|
optional ResponseEngineStateChanged response_engine_state_changed = 19;
|
||||||
optional ResponseUpdateTrackPosition response_update_track_position = 20;
|
optional ResponseUpdateTrackPosition response_update_track_position = 20;
|
||||||
|
optional ResponseDisconnect response_disconnect = 22;
|
||||||
}
|
}
|
||||||
|
10
src/main.cpp
10
src/main.cpp
@ -104,6 +104,10 @@ using boost::scoped_ptr;
|
|||||||
# include "devices/wmdmthread.h"
|
# include "devices/wmdmthread.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef Q_OS_WIN32
|
||||||
|
#include <signal.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
// Load sqlite plugin on windows and mac.
|
// Load sqlite plugin on windows and mac.
|
||||||
#ifdef HAVE_STATIC_SQLITE
|
#ifdef HAVE_STATIC_SQLITE
|
||||||
# include <QtPlugin>
|
# include <QtPlugin>
|
||||||
@ -406,6 +410,12 @@ int main(int argc, char *argv[]) {
|
|||||||
qtsparkle::LoadTranslations(language);
|
qtsparkle::LoadTranslations(language);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef Q_OS_WIN32
|
||||||
|
// This is needed to prevent SIGPIPE Errors, which occur under some
|
||||||
|
// circumstances in RemoteClient. They cause a program termination.
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
IconLoader::Init();
|
IconLoader::Init();
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ IncomingDataParser::IncomingDataParser(Application* app)
|
|||||||
app_->player(), SLOT(SetVolume(int)));
|
app_->player(), SLOT(SetVolume(int)));
|
||||||
connect(this, SIGNAL(PlayAt(int,Engine::TrackChangeFlags,bool)),
|
connect(this, SIGNAL(PlayAt(int,Engine::TrackChangeFlags,bool)),
|
||||||
app_->player(), SLOT(PlayAt(int,Engine::TrackChangeFlags,bool)));
|
app_->player(), SLOT(PlayAt(int,Engine::TrackChangeFlags,bool)));
|
||||||
|
connect(this, SIGNAL(SeekTo(int)),
|
||||||
|
app_->player(), SLOT(SeekTo(int)));
|
||||||
|
|
||||||
// For some connects we have to wait for the playlistmanager
|
// For some connects we have to wait for the playlistmanager
|
||||||
// to be initialized
|
// to be initialized
|
||||||
@ -69,16 +71,9 @@ bool IncomingDataParser::close_connection() {
|
|||||||
return close_connection_;
|
return close_connection_;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IncomingDataParser::Parse(const QByteArray& data) {
|
void IncomingDataParser::Parse(const pb::remote::Message& msg) {
|
||||||
close_connection_ = false;
|
close_connection_ = false;
|
||||||
|
|
||||||
// Parse the incoming data
|
|
||||||
pb::remote::Message msg;
|
|
||||||
if (!msg.ParseFromArray(data.constData(), data.size())) {
|
|
||||||
qLog(Info) << "Couldn't parse data";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check what's to do
|
// Now check what's to do
|
||||||
switch (msg.type()) {
|
switch (msg.type()) {
|
||||||
case pb::remote::CONNECT: emit SendClementineInfo();
|
case pb::remote::CONNECT: emit SendClementineInfo();
|
||||||
@ -112,6 +107,9 @@ void IncomingDataParser::Parse(const QByteArray& data) {
|
|||||||
break;
|
break;
|
||||||
case pb::remote::SHUFFLE: SetShuffleMode(msg.shuffle());
|
case pb::remote::SHUFFLE: SetShuffleMode(msg.shuffle());
|
||||||
break;
|
break;
|
||||||
|
case pb::remote::SET_TRACK_POSITION:
|
||||||
|
emit SeekTo(msg.request_set_track_position().position());
|
||||||
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ public:
|
|||||||
bool close_connection();
|
bool close_connection();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void Parse(const QByteArray& pb_data);
|
void Parse(const pb::remote::Message& msg);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void SendClementineInfo();
|
void SendClementineInfo();
|
||||||
@ -34,6 +34,7 @@ signals:
|
|||||||
void ShuffleCurrent();
|
void ShuffleCurrent();
|
||||||
void SetRepeatMode(PlaylistSequence::RepeatMode mode);
|
void SetRepeatMode(PlaylistSequence::RepeatMode mode);
|
||||||
void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
|
void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
|
||||||
|
void SeekTo(int seconds);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void PlaylistManagerInitialized();
|
void PlaylistManagerInitialized();
|
||||||
|
@ -65,6 +65,10 @@ void NetworkRemote::SetupServer() {
|
|||||||
SIGNAL(ArtLoaded(const Song&, const QString&, const QImage&)),
|
SIGNAL(ArtLoaded(const Song&, const QString&, const QImage&)),
|
||||||
outgoing_data_creator_.get(),
|
outgoing_data_creator_.get(),
|
||||||
SLOT(CurrentSongChanged(const Song&, const QString&, const QImage&)));
|
SLOT(CurrentSongChanged(const Song&, const QString&, const QImage&)));
|
||||||
|
|
||||||
|
// Only connect the signals once
|
||||||
|
connect(server_.get(), SIGNAL(newConnection()), this, SLOT(AcceptConnection()));
|
||||||
|
connect(server_ipv6_.get(), SIGNAL(newConnection()), this, SLOT(AcceptConnection()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkRemote::StartServer() {
|
void NetworkRemote::StartServer() {
|
||||||
@ -81,9 +85,6 @@ void NetworkRemote::StartServer() {
|
|||||||
|
|
||||||
qLog(Info) << "Starting network remote";
|
qLog(Info) << "Starting network remote";
|
||||||
|
|
||||||
connect(server_.get(), SIGNAL(newConnection()), this, SLOT(AcceptConnection()));
|
|
||||||
connect(server_ipv6_.get(), SIGNAL(newConnection()), this, SLOT(AcceptConnection()));
|
|
||||||
|
|
||||||
server_->listen(QHostAddress::Any, port_);
|
server_->listen(QHostAddress::Any, port_);
|
||||||
server_ipv6_->listen(QHostAddress::AnyIPv6, port_);
|
server_ipv6_->listen(QHostAddress::AnyIPv6, port_);
|
||||||
|
|
||||||
@ -97,6 +98,7 @@ void NetworkRemote::StartServer() {
|
|||||||
|
|
||||||
void NetworkRemote::StopServer() {
|
void NetworkRemote::StopServer() {
|
||||||
if (server_->isListening()) {
|
if (server_->isListening()) {
|
||||||
|
outgoing_data_creator_.get()->DisconnectAllClients();
|
||||||
server_->close();
|
server_->close();
|
||||||
server_ipv6_->close();
|
server_ipv6_->close();
|
||||||
clients_.clear();
|
clients_.clear();
|
||||||
@ -176,7 +178,7 @@ void NetworkRemote::CreateRemoteClient(QTcpSocket *client_socket) {
|
|||||||
clients_.push_back(client);
|
clients_.push_back(client);
|
||||||
|
|
||||||
// Connect the signal to parse data
|
// Connect the signal to parse data
|
||||||
connect(client, SIGNAL(Parse(QByteArray)),
|
connect(client, SIGNAL(Parse(pb::remote::Message)),
|
||||||
incoming_data_parser_.get(), SLOT(Parse(QByteArray)));
|
incoming_data_parser_.get(), SLOT(Parse(pb::remote::Message)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,9 @@ void OutgoingDataCreator::SendFirstData() {
|
|||||||
|
|
||||||
// then the current volume
|
// then the current volume
|
||||||
VolumeChanged(app_->player()->GetVolume());
|
VolumeChanged(app_->player()->GetVolume());
|
||||||
|
|
||||||
|
// And the current track position
|
||||||
|
UpdateTrackPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutgoingDataCreator::CurrentSongChanged(const Song& song, const QString& uri, const QImage& img) {
|
void OutgoingDataCreator::CurrentSongChanged(const Song& song, const QString& uri, const QImage& img) {
|
||||||
@ -340,3 +343,10 @@ void OutgoingDataCreator::UpdateTrackPosition() {
|
|||||||
|
|
||||||
SendDataToClients(&msg);
|
SendDataToClients(&msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OutgoingDataCreator::DisconnectAllClients() {
|
||||||
|
pb::remote::Message msg;
|
||||||
|
msg.set_type(pb::remote::DISCONNECT);
|
||||||
|
msg.mutable_response_disconnect()->set_reason_disconnect(pb::remote::Server_Shutdown);
|
||||||
|
SendDataToClients(&msg);
|
||||||
|
}
|
||||||
|
@ -37,6 +37,7 @@ public slots:
|
|||||||
void SendRepeatMode(PlaylistSequence::RepeatMode mode);
|
void SendRepeatMode(PlaylistSequence::RepeatMode mode);
|
||||||
void SendShuffleMode(PlaylistSequence::ShuffleMode mode);
|
void SendShuffleMode(PlaylistSequence::ShuffleMode mode);
|
||||||
void UpdateTrackPosition();
|
void UpdateTrackPosition();
|
||||||
|
void DisconnectAllClients();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Application* app_;
|
Application* app_;
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
|
|
||||||
#include "remoteclient.h"
|
#include "remoteclient.h"
|
||||||
|
#include "networkremote.h"
|
||||||
|
|
||||||
#include <QDataStream>
|
#include <QDataStream>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
RemoteClient::RemoteClient(Application* app, QTcpSocket* client)
|
RemoteClient::RemoteClient(Application* app, QTcpSocket* client)
|
||||||
: app_(app),
|
: app_(app),
|
||||||
@ -32,6 +34,21 @@ RemoteClient::RemoteClient(Application* app, QTcpSocket* client)
|
|||||||
|
|
||||||
// Connect to the slot IncomingData when receiving data
|
// Connect to the slot IncomingData when receiving data
|
||||||
connect(client, SIGNAL(readyRead()), this, SLOT(IncomingData()));
|
connect(client, SIGNAL(readyRead()), this, SLOT(IncomingData()));
|
||||||
|
|
||||||
|
// Connect the signals to see if an error occured or the client
|
||||||
|
// was disconnected.
|
||||||
|
connect(client, SIGNAL(disconnected()), this, SLOT(Disconnected()));
|
||||||
|
connect(client, SIGNAL(error(QAbstractSocket::SocketError)),
|
||||||
|
this, SLOT(Error(QAbstractSocket::SocketError)));
|
||||||
|
|
||||||
|
// Check if we use auth code
|
||||||
|
QSettings s;
|
||||||
|
|
||||||
|
s.beginGroup(NetworkRemote::kSettingsGroup);
|
||||||
|
use_auth_code_ = s.value("use_auth_code", false).toBool();
|
||||||
|
auth_code_ = s.value("auth_code", 0).toInt();
|
||||||
|
|
||||||
|
s.endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -54,7 +71,7 @@ void RemoteClient::IncomingData() {
|
|||||||
// Did we get everything?
|
// Did we get everything?
|
||||||
if (buffer_.size() == expected_length_) {
|
if (buffer_.size() == expected_length_) {
|
||||||
// Parse the message
|
// Parse the message
|
||||||
emit Parse(buffer_.data());
|
ParseMessage(buffer_.data());
|
||||||
|
|
||||||
// Clear the buffer
|
// Clear the buffer
|
||||||
buffer_.close();
|
buffer_.close();
|
||||||
@ -65,19 +82,61 @@ void RemoteClient::IncomingData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemoteClient::ParseMessage(const QByteArray &data) {
|
||||||
|
pb::remote::Message msg;
|
||||||
|
if (!msg.ParseFromArray(data.constData(), data.size())) {
|
||||||
|
qLog(Info) << "Couldn't parse data";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type() == pb::remote::CONNECT && use_auth_code_) {
|
||||||
|
if (msg.request_connect().auth_code() != auth_code_) {
|
||||||
|
DisconnectClient();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now parse the other data
|
||||||
|
emit Parse(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteClient::DisconnectClient() {
|
||||||
|
pb::remote::Message msg;
|
||||||
|
msg.set_type(pb::remote::DISCONNECT);
|
||||||
|
msg.mutable_response_disconnect()->set_reason_disconnect(pb::remote::Wrong_Auth_Code);
|
||||||
|
SendData(&msg);
|
||||||
|
|
||||||
|
// Just close the connection. The next time the outgoing data creator
|
||||||
|
// sends a keep alive, the client will be deleted
|
||||||
|
client_->close();
|
||||||
|
}
|
||||||
|
|
||||||
void RemoteClient::SendData(pb::remote::Message *msg) {
|
void RemoteClient::SendData(pb::remote::Message *msg) {
|
||||||
// Serialize the message
|
// Serialize the message
|
||||||
std::string data = msg->SerializeAsString();
|
std::string data = msg->SerializeAsString();
|
||||||
|
|
||||||
// write the length of the data first
|
// write the length of the data first
|
||||||
QDataStream s(client_);
|
if (client_->isWritable()) {
|
||||||
s << qint32(data.length());
|
QDataStream s(client_);
|
||||||
s.writeRawData(data.data(), data.length());
|
s << qint32(data.length());
|
||||||
|
s.writeRawData(data.data(), data.length());
|
||||||
|
|
||||||
// Flush data
|
// Flush data
|
||||||
client_->flush();
|
client_->flush();
|
||||||
|
} else {
|
||||||
|
client_->close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QAbstractSocket::SocketState RemoteClient::State() {
|
QAbstractSocket::SocketState RemoteClient::State() {
|
||||||
return client_->state();
|
return client_->state();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemoteClient::Disconnected() {
|
||||||
|
qLog(Info) << "Client Disconnected";
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteClient::Error(QAbstractSocket::SocketError socket_error) {
|
||||||
|
qLog(Info) << "Client Error:" << socket_error;
|
||||||
|
client_->close();
|
||||||
|
}
|
||||||
|
@ -21,11 +21,19 @@ private slots:
|
|||||||
void IncomingData();
|
void IncomingData();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void Parse(const QByteArray& pb_data);
|
void Parse(const pb::remote::Message& msg);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void ParseMessage(const QByteArray& data);
|
||||||
|
void DisconnectClient();
|
||||||
|
void Disconnected();
|
||||||
|
void Error(QAbstractSocket::SocketError);
|
||||||
|
|
||||||
Application* app_;
|
Application* app_;
|
||||||
|
|
||||||
|
bool use_auth_code_;
|
||||||
|
int auth_code_;
|
||||||
|
|
||||||
QTcpSocket* client_;
|
QTcpSocket* client_;
|
||||||
bool reading_protobuf_;
|
bool reading_protobuf_;
|
||||||
quint32 expected_length_;
|
quint32 expected_length_;
|
||||||
|
@ -59,6 +59,10 @@ void NetworkRemoteSettingsPage::Load() {
|
|||||||
s.setValue("only_non_public_ip", true);
|
s.setValue("only_non_public_ip", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth Code, 5 digits
|
||||||
|
ui_->use_auth_code->setChecked(s.value("use_auth_code", false).toBool());
|
||||||
|
ui_->auth_code->setValue(s.value("auth_code", qrand() % 100000).toInt());
|
||||||
|
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
QPixmap android_qr_code(":clementine_remote_qr.png");
|
QPixmap android_qr_code(":clementine_remote_qr.png");
|
||||||
@ -72,6 +76,8 @@ void NetworkRemoteSettingsPage::Save() {
|
|||||||
s.setValue("port", ui_->remote_port->value());
|
s.setValue("port", ui_->remote_port->value());
|
||||||
s.setValue("use_remote", ui_->use_remote->isChecked());
|
s.setValue("use_remote", ui_->use_remote->isChecked());
|
||||||
s.setValue("only_non_public_ip", ui_->only_non_public_ip->isChecked());
|
s.setValue("only_non_public_ip", ui_->only_non_public_ip->isChecked());
|
||||||
|
s.setValue("use_auth_code", ui_->use_auth_code->isChecked());
|
||||||
|
s.setValue("auth_code", ui_->auth_code->value());
|
||||||
|
|
||||||
s.endGroup();
|
s.endGroup();
|
||||||
|
|
||||||
|
@ -83,6 +83,33 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="use_auth_code">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>A client can connect only, if the correct code was entered.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Use authentication code</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="auth_code">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="suffix">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>99999</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -146,5 +173,21 @@
|
|||||||
</hint>
|
</hint>
|
||||||
</hints>
|
</hints>
|
||||||
</connection>
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>use_auth_code</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>auth_code</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>137</x>
|
||||||
|
<y>124</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>351</x>
|
||||||
|
<y>125</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
</connections>
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user