mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-13 16:18:42 +00:00
Live Map Chart (#3487)
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
#include "WorkoutPlotWindow.h"
|
||||
#include "WorkoutWindow.h"
|
||||
#include "WebPageWindow.h"
|
||||
#include "LiveMapWebPageWindow.h"
|
||||
#ifdef GC_WANT_R
|
||||
#include "RChart.h"
|
||||
#endif
|
||||
@@ -76,7 +77,7 @@ GcWindowRegistry* GcWindows;
|
||||
void
|
||||
GcWindowRegistry::initialize()
|
||||
{
|
||||
static GcWindowRegistry GcWindowsInit[35] = {
|
||||
static GcWindowRegistry GcWindowsInit[34] = {
|
||||
// name GcWinID
|
||||
{ VIEW_HOME|VIEW_DIARY, tr("Overview "),GcWindowTypes::OverviewTrends },
|
||||
{ VIEW_HOME|VIEW_DIARY, tr("User Chart"),GcWindowTypes::UserTrends },
|
||||
@@ -115,6 +116,7 @@ GcWindowRegistry::initialize()
|
||||
{ VIEW_TRAIN, tr("Pedal Stroke"),GcWindowTypes::SpinScanPlot },
|
||||
{ VIEW_TRAIN, tr("Video Player"),GcWindowTypes::VideoPlayer },
|
||||
{ VIEW_TRAIN, tr("Workout Editor"),GcWindowTypes::WorkoutWindow },
|
||||
{ VIEW_TRAIN, tr("Live Map"),GcWindowTypes::LiveMapWebPageWindow },
|
||||
{ VIEW_ANALYSIS|VIEW_HOME|VIEW_TRAIN, tr("Web page"),GcWindowTypes::WebPageWindow },
|
||||
{ 0, "", GcWindowTypes::None }};
|
||||
// initialize the global registry
|
||||
@@ -237,6 +239,7 @@ GcWindowRegistry::newGcWindow(GcWinID id, Context *context)
|
||||
case GcWindowTypes::WorkoutWindow: returning = new WorkoutWindow(context); break;
|
||||
|
||||
case GcWindowTypes::WebPageWindow: returning = new WebPageWindow(context); break;
|
||||
case GcWindowTypes::LiveMapWebPageWindow: returning = new LiveMapWebPageWindow(context); break;
|
||||
#if 0 // not till v4.0
|
||||
case GcWindowTypes::RouteSegment: returning = new RouteWindow(context); break;
|
||||
#else
|
||||
|
||||
@@ -73,7 +73,9 @@ enum gcwinid {
|
||||
PythonSeason = 44,
|
||||
UserTrends=45,
|
||||
UserAnalysis=46,
|
||||
OverviewTrends=47
|
||||
OverviewTrends=47,
|
||||
LiveMapWebPageWindow = 48
|
||||
|
||||
};
|
||||
};
|
||||
typedef enum GcWindowTypes::gcwinid GcWinID;
|
||||
|
||||
301
src/Train/LiveMapWebPageWindow.cpp
Normal file
301
src/Train/LiveMapWebPageWindow.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Copyright (c) 2020 Peter Kanatselis (pkanatselis@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
#include "LiveMapWebPageWindow.h"
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "RideItem.h"
|
||||
#include "RideFile.h"
|
||||
#include "RideImportWizard.h"
|
||||
#include "IntervalItem.h"
|
||||
#include "IntervalTreeView.h"
|
||||
#include "SmallPlot.h"
|
||||
#include "Context.h"
|
||||
#include "Athlete.h"
|
||||
#include "Zones.h"
|
||||
#include "Settings.h"
|
||||
#include "Colors.h"
|
||||
#include "Units.h"
|
||||
#include "TimeUtils.h"
|
||||
#include "HelpWhatsThis.h"
|
||||
#include "Library.h"
|
||||
#include "ErgFile.h"
|
||||
#include "LocationInterpolation.h"
|
||||
|
||||
//#include <QtWebChannel>
|
||||
//#include <QWebEngineProfile>
|
||||
|
||||
// overlay helper
|
||||
#include "TabView.h"
|
||||
#include "GcOverlayWidget.h"
|
||||
#include "IntervalSummaryWindow.h"
|
||||
|
||||
// declared in main, we only want to use it to get QStyle
|
||||
extern QApplication *application;
|
||||
|
||||
LiveMapWebPageWindow::LiveMapWebPageWindow(Context *context) : GcChartWindow(context), context(context)
|
||||
{
|
||||
// Connect signal to receive updates on lat/lon for ploting on map.
|
||||
connect(context, SIGNAL(telemetryUpdate(RealtimeData)), this, SLOT(telemetryUpdate(RealtimeData)));
|
||||
connect(context, SIGNAL(stop()), this, SLOT(stop()));
|
||||
connect(context, SIGNAL(ergFileSelected(ErgFile*)), this, SLOT(ergFileSelected(ErgFile*)));
|
||||
|
||||
// reveal controls widget
|
||||
// layout reveal controls
|
||||
QHBoxLayout *revealLayout = new QHBoxLayout;
|
||||
revealLayout->setContentsMargins(0,0,0,0);
|
||||
|
||||
rButton = new QPushButton(application->style()->standardIcon(QStyle::SP_ArrowRight), "", this);
|
||||
rCustomUrl = new QLineEdit(this);
|
||||
revealLayout->addStretch();
|
||||
revealLayout->addWidget(rButton);
|
||||
revealLayout->addWidget(rCustomUrl);
|
||||
revealLayout->addStretch();
|
||||
setRevealLayout(revealLayout);
|
||||
|
||||
connect(rButton, SIGNAL(clicked(bool)), this, SLOT(userUrl()));
|
||||
|
||||
// Chart settings
|
||||
QWidget * settingsWidget = new QWidget(this);
|
||||
settingsWidget->setContentsMargins(0,0,0,0);
|
||||
setProperty("color", GColor(CTRAINPLOTBACKGROUND));
|
||||
|
||||
QFormLayout* commonLayout = new QFormLayout(settingsWidget);
|
||||
|
||||
QString sValue = "";
|
||||
customUrlLabel = new QLabel(tr("OSM Base URL"));
|
||||
customUrl = new QLineEdit(this);
|
||||
customUrl->setFixedWidth(300);
|
||||
|
||||
if (customUrl->text() == "") {
|
||||
customUrl->setText("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png");
|
||||
}
|
||||
commonLayout->addRow(customUrlLabel, customUrl);
|
||||
|
||||
connect(customUrlLabel, SIGNAL(returnPressed()), this, SLOT(userUrl()));
|
||||
|
||||
applyButton = new QPushButton(application->style()->standardIcon(QStyle::SP_ArrowRight), tr("Apply changes"), this);
|
||||
commonLayout->addRow(applyButton);
|
||||
|
||||
// Connect signal to update map
|
||||
connect(applyButton, SIGNAL(clicked(bool)), this, SLOT(userUrl()));
|
||||
|
||||
setControls(settingsWidget);
|
||||
setContentsMargins(0, 0, 0, 0);
|
||||
layout = new QVBoxLayout();
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(2, 2, 2, 2);
|
||||
setChartLayout(layout);
|
||||
|
||||
// set webview for map
|
||||
view = new QWebEngineView(this);
|
||||
webPage = view->page();
|
||||
view->setPage(webPage);
|
||||
|
||||
view->setContentsMargins(0,0,0,0);
|
||||
view->page()->view()->setContentsMargins(0,10,0,0);
|
||||
view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
view->setAcceptDrops(false);
|
||||
layout->addWidget(view);
|
||||
|
||||
configChanged(CONFIG_APPEARANCE);
|
||||
|
||||
// Finish initialization using current settings
|
||||
userUrl();
|
||||
ergFileSelected(context->currentErgFile());
|
||||
}
|
||||
|
||||
void LiveMapWebPageWindow::userUrl()
|
||||
{
|
||||
// add http:// if scheme is missing
|
||||
QRegExp hasscheme("^[^:]*://.*");
|
||||
QString url = rCustomUrl->text();
|
||||
if (!hasscheme.exactMatch(url)) url = "http://" + url;
|
||||
view->setZoomFactor(dpiXFactor);
|
||||
view->setUrl(QUrl(url));
|
||||
}
|
||||
|
||||
LiveMapWebPageWindow::~LiveMapWebPageWindow()
|
||||
{
|
||||
}
|
||||
|
||||
void LiveMapWebPageWindow::ergFileSelected(ErgFile* f)
|
||||
{
|
||||
// rename window to workout name and draw route if data exists
|
||||
if (f && f->filename != "" )
|
||||
{
|
||||
setIsBlank(false);
|
||||
QString startingLat = QString::number(((f->Points)[0]).lat);
|
||||
QString startingLon = QString::number(((f->Points)[0]).lon);
|
||||
if (startingLat == "0" && startingLon == "0")
|
||||
{
|
||||
markerIsVisible = false;
|
||||
setIsBlank(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
QString js = ("<div><script type=\"text/javascript\">initMap (" + startingLat + ", " + startingLon + ",13);</script></div>\n");
|
||||
routeLatLngs = "[";
|
||||
QString code = "";
|
||||
|
||||
for (int pt = 0; pt < f->Points.size() - 1; pt++) {
|
||||
|
||||
geolocation geoloc(f->Points[pt].lat, f->Points[pt].lon, f->Points[pt].y);
|
||||
if (geoloc.IsReasonableGeoLocation()) {
|
||||
if (pt == 0) { routeLatLngs += "["; }
|
||||
else { routeLatLngs += ",["; }
|
||||
routeLatLngs += QVariant(f->Points[pt].lat).toString();
|
||||
routeLatLngs += ",";
|
||||
routeLatLngs += QVariant(f->Points[pt].lon).toString();
|
||||
routeLatLngs += "]";
|
||||
}
|
||||
|
||||
}
|
||||
routeLatLngs += "]";
|
||||
// We can either setHTML page or runJavaScript but not both.
|
||||
// So we create divs with the 2 methods we need to run when the document loads
|
||||
code = QString("showRoute (" + routeLatLngs + ");");
|
||||
js += ("<div><script type=\"text/javascript\">" + code + "</script></div>\n");
|
||||
createHtml(customUrl->text(), js);
|
||||
view->page()->setHtml(currentPage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
markerIsVisible = false;
|
||||
setIsBlank(true);
|
||||
}
|
||||
}
|
||||
|
||||
void LiveMapWebPageWindow::drawRoute(ErgFile* f) {
|
||||
routeLatLngs = "[";
|
||||
QString code = "";
|
||||
|
||||
for (int pt = 0; pt < f->Points.size() - 1; pt++) {
|
||||
|
||||
geolocation geoloc(f->Points[pt].lat, f->Points[pt].lon, f->Points[pt].y);
|
||||
if (geoloc.IsReasonableGeoLocation()) {
|
||||
if (pt == 0) { routeLatLngs += "["; }
|
||||
else { routeLatLngs += ",["; }
|
||||
routeLatLngs += QVariant(f->Points[pt].lat).toString();
|
||||
routeLatLngs += ",";
|
||||
routeLatLngs += QVariant(f->Points[pt].lon).toString();
|
||||
routeLatLngs += "]";
|
||||
}
|
||||
|
||||
}
|
||||
routeLatLngs += "]";
|
||||
code = QString("showRoute (" + routeLatLngs + ");");
|
||||
view->page()->runJavaScript(code);
|
||||
}
|
||||
|
||||
// Reset map to preferred View when the activity is stopped.
|
||||
void LiveMapWebPageWindow::stop()
|
||||
{
|
||||
markerIsVisible = false;
|
||||
}
|
||||
|
||||
void LiveMapWebPageWindow::configChanged(qint32)
|
||||
{
|
||||
// tinted palette for headings etc
|
||||
QPalette palette;
|
||||
palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND)));
|
||||
palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
|
||||
palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
|
||||
palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
|
||||
setPalette(palette);
|
||||
|
||||
}
|
||||
|
||||
// Update position on the map when telemetry changes.
|
||||
void LiveMapWebPageWindow::telemetryUpdate(RealtimeData rtd)
|
||||
{
|
||||
QString code = "";
|
||||
geolocation geoloc(rtd.getLatitude(), rtd.getLongitude(), rtd.getAltitude());
|
||||
if (geoloc.IsReasonableGeoLocation()) {
|
||||
QString sLat = QVariant(rtd.getLatitude()).toString();
|
||||
QString sLon = QVariant(rtd.getLongitude()).toString();
|
||||
code = "";
|
||||
if (!markerIsVisible)
|
||||
{
|
||||
code = QString("centerMap (" + sLat + ", " + sLon + ", " + "15" + ");");
|
||||
code += QString("showMyMarker (" + sLat + ", " + sLon + ");");
|
||||
markerIsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
code += QString("moveMarker (" + sLat + ", " + sLon + ");");
|
||||
}
|
||||
view->page()->runJavaScript(code);
|
||||
}
|
||||
}
|
||||
// Build HTML code with all the javascript functions to be called later
|
||||
// to update the postion on the mapp
|
||||
void LiveMapWebPageWindow::createHtml(QString sBaseUrl, QString autoRunJS)
|
||||
{
|
||||
currentPage = "";
|
||||
|
||||
currentPage = QString("<html><head>\n"
|
||||
"<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=yes\"/> \n"
|
||||
"<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\"/>\n"
|
||||
"<title>GoldenCheetah LiveMap - TrainView</title>\n"
|
||||
"<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.css\"\n"
|
||||
"integrity=\"sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==\" crossorigin=\"\"/>\n"
|
||||
"<script src=\"https://unpkg.com/leaflet@1.6.0/dist/leaflet.js\"\n"
|
||||
"integrity=\"sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==\" crossorigin=\"\"></script>\n"
|
||||
"<style>#mapid {height:100%;width:100%}</style></head>\n"
|
||||
"<body><div id=\"mapid\"></div>\n"
|
||||
"<script type=\"text/javascript\">\n"
|
||||
"var mapOptions, mymap, mylayer, mymarker, latlng, myscale, routepolyline\n"
|
||||
"function moveMarker(myLat, myLon) {\n"
|
||||
" mymap.panTo(new L.LatLng(myLat, myLon));\n"
|
||||
" mymarker.setLatLng(new L.latLng(myLat, myLon));\n"
|
||||
"}\n"
|
||||
"function initMap(myLat, myLon, myZoom) {\n"
|
||||
" mapOptions = {\n"
|
||||
" center: [myLat, myLon],\n"
|
||||
" zoom : myZoom,\n"
|
||||
" zoomControl : true,\n"
|
||||
" scrollWheelZoom : false,\n"
|
||||
" dragging : false,\n"
|
||||
" doubleClickZoom : false }\n"
|
||||
" mymap = L.map('mapid', mapOptions);\n"
|
||||
" myscale = L.control.scale().addTo(mymap);\n"
|
||||
" mylayer = new L.tileLayer('" + sBaseUrl +"');\n"
|
||||
" mymap.addLayer(mylayer);\n"
|
||||
"}\n"
|
||||
"function showMyMarker(myLat, myLon) {\n"
|
||||
" mymarker = new L.marker([myLat, myLon], {\n"
|
||||
" draggable: false,\n"
|
||||
" title : \"GoldenCheetah - Workout LiveMap\",\n"
|
||||
" alt : \"GoldenCheetah - Workout LiveMap\",\n"
|
||||
" riseOnHover : true\n"
|
||||
" }).addTo(mymap);\n"
|
||||
"}\n"
|
||||
"function centerMap(myLat, myLon, myZoom) {\n"
|
||||
" latlng = L.latLng(myLat, myLon);\n"
|
||||
" mymap.setView(latlng, myZoom)\n"
|
||||
"}\n"
|
||||
"function showRoute(myRouteLatlngs) {\n"
|
||||
" routepolyline = L.polyline(myRouteLatlngs, { color: 'red' }).addTo(mymap);\n"
|
||||
" mymap.fitBounds(routepolyline.getBounds());\n"
|
||||
"}\n"
|
||||
"</script>\n"
|
||||
+ autoRunJS +
|
||||
"</body></html>\n"
|
||||
);
|
||||
}
|
||||
114
src/Train/LiveMapWebPageWindow.h
Normal file
114
src/Train/LiveMapWebPageWindow.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2016 Damien Grauser (Damien.Grauser@gmail.com)
|
||||
* updated (c) 2020 Peter Kanatselis (pkanatselis@gmail.com)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 51
|
||||
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _GC_LiveMapWebPageWindow_h
|
||||
#define _GC_LiveMapWebPageWindow_h
|
||||
#include "GoldenCheetah.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QDialog>
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include "RideFile.h"
|
||||
#include "IntervalItem.h"
|
||||
#include "Context.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSslSocket>
|
||||
#include <QWebEnginePage>
|
||||
#include <QWebEngineView>
|
||||
|
||||
|
||||
class QMouseEvent;
|
||||
class RideItem;
|
||||
class Context;
|
||||
class QColor;
|
||||
class QVBoxLayout;
|
||||
class QTabWidget;
|
||||
class LLiveMapWebPageWindow;
|
||||
class IntervalSummaryWindow;
|
||||
class SmallPlot;
|
||||
|
||||
class LiveMapsimpleWebPage : public QWebEnginePage
|
||||
{
|
||||
};
|
||||
|
||||
class LiveMapWebPageWindow : public GcChartWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
// properties can be saved/restored/set by the layout manager
|
||||
Q_PROPERTY(QString url READ url WRITE setUrl USER true)
|
||||
|
||||
public:
|
||||
LiveMapWebPageWindow(Context *);
|
||||
~LiveMapWebPageWindow();
|
||||
bool markerIsVisible;
|
||||
double plotLon = 0;
|
||||
double plotLat = 0;
|
||||
QString currentPage;
|
||||
QString routeLatLngs;
|
||||
|
||||
// set/get properties
|
||||
QString url() const { return customUrl->text(); }
|
||||
void setUrl(QString x) { customUrl->setText(x); }
|
||||
|
||||
public slots:
|
||||
void configChanged(qint32);
|
||||
void ergFileSelected(ErgFile*);
|
||||
void userUrl();
|
||||
|
||||
private:
|
||||
Context *context;
|
||||
QVBoxLayout *layout;
|
||||
QComboBox* mapCombo;
|
||||
|
||||
QWebEngineView *view;
|
||||
QWebEnginePage* webPage;
|
||||
LiveMapWebPageWindow(); // default ctor
|
||||
// setting dialog
|
||||
QLabel* customUrlLabel;
|
||||
QLabel* customLonLabel;
|
||||
QLabel* customLatLabel;
|
||||
QLabel* customZoomLabel;
|
||||
QLineEdit* customUrl;
|
||||
// reveal controls
|
||||
QLineEdit* rCustomUrl;
|
||||
QLineEdit* customLat;
|
||||
QLineEdit* customLon;
|
||||
QLineEdit* customZoom;
|
||||
QPushButton* rButton;
|
||||
QPushButton* applyButton;
|
||||
|
||||
void createHtml(QString sBaseUrl, QString autoRunJS);
|
||||
void drawRoute(ErgFile* f);
|
||||
|
||||
private slots:
|
||||
void telemetryUpdate(RealtimeData rtd);
|
||||
void stop();
|
||||
|
||||
protected:
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -756,7 +756,8 @@ greaterThan(QT_MAJOR_VERSION, 4) {
|
||||
|
||||
HEADERS += Train/TrainBottom.h Train/TrainDB.h Train/TrainSidebar.h \
|
||||
Train/VideoLayoutParser.h Train/VideoSyncFile.h Train/WorkoutPlotWindow.h Train/WebPageWindow.h \
|
||||
Train/WorkoutWidget.h Train/WorkoutWidgetItems.h Train/WorkoutWindow.h Train/WorkoutWizard.h Train/ZwoParser.h
|
||||
Train/WorkoutWidget.h Train/WorkoutWidgetItems.h Train/WorkoutWindow.h Train/WorkoutWizard.h Train/ZwoParser.h \
|
||||
Train/LiveMapWebPageWindow.h
|
||||
|
||||
|
||||
###=============
|
||||
@@ -859,7 +860,8 @@ greaterThan(QT_MAJOR_VERSION, 4) {
|
||||
|
||||
SOURCES += Train/TrainBottom.cpp Train/TrainDB.cpp Train/TrainSidebar.cpp \
|
||||
Train/VideoLayoutParser.cpp Train/VideoSyncFile.cpp Train/WorkoutPlotWindow.cpp Train/WebPageWindow.cpp \
|
||||
Train/WorkoutWidget.cpp Train/WorkoutWidgetItems.cpp Train/WorkoutWindow.cpp Train/WorkoutWizard.cpp Train/ZwoParser.cpp
|
||||
Train/WorkoutWidget.cpp Train/WorkoutWidgetItems.cpp Train/WorkoutWindow.cpp Train/WorkoutWizard.cpp Train/ZwoParser.cpp \
|
||||
Train/LiveMapWebPageWindow.cpp
|
||||
|
||||
## Crash Handling
|
||||
win32-msvc* {
|
||||
|
||||
Reference in New Issue
Block a user