mirror of
https://github.com/subsurface/subsurface.git
synced 2025-01-31 19:13:24 +00:00
Changed Facebook stuff to socialnetworks.h/cpp
All Facebook related stuff now is on SocialNetworks.h/cpp this makes it much easier to implement things and looking for bugs. working: - logging in - getting user id - getting album id ( or creating it ) *much* more testing is needed, of course. Signed-off-by: Tomaz Canabrava <tomaz.canabrava@intel.com> Signed-off-by: Dirk Hohndel <dirk@hohndel.org>
This commit is contained in:
parent
c8c17dc01e
commit
ee5d93e155
11 changed files with 247 additions and 154 deletions
4
pref.h
4
pref.h
|
@ -19,9 +19,9 @@ typedef struct
|
|||
} partial_pressure_graphs_t;
|
||||
|
||||
typedef struct {
|
||||
char *user_id;
|
||||
char *access_token;
|
||||
char *album_name;
|
||||
char *user_id;
|
||||
char *album_id;
|
||||
} facebook_prefs_t;
|
||||
|
||||
struct preferences {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <QFileDialog>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "socialnetworks.h"
|
||||
#include "../qthelper.h"
|
||||
|
||||
// # Date Rtg Dpth Dur Tmp Wght Suit Cyl Gas SAC OTU CNS Loc
|
||||
|
@ -842,8 +843,7 @@ void DiveListView::contextMenuEvent(QContextMenuEvent *event)
|
|||
|
||||
void DiveListView::publishFacebook()
|
||||
{
|
||||
FacebookManager manager;
|
||||
manager.checkAlbumExists();
|
||||
FacebookManager *fb = FacebookManager::instance();
|
||||
}
|
||||
|
||||
void DiveListView::shiftTimes()
|
||||
|
|
|
@ -11,14 +11,7 @@
|
|||
#include <QNetworkReply>
|
||||
#include <QWebView>
|
||||
#include <QJsonDocument>
|
||||
|
||||
|
||||
static QString facebookConnectUrl =
|
||||
"https://www.facebook.com/dialog/oauth?"
|
||||
"client_id=427722490709000"
|
||||
"&redirect_uri=http://www.facebook.com/connect/login_success.html"
|
||||
"&response_type=token"
|
||||
"&scope=publish_actions,user_photos";
|
||||
#include "socialnetworks.h"
|
||||
|
||||
PreferencesDialog *PreferencesDialog::instance()
|
||||
{
|
||||
|
@ -39,20 +32,18 @@ PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WindowFlags f) : QDial
|
|||
ui.proxyType->setCurrentIndex(-1);
|
||||
|
||||
// Facebook stuff:
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup("WebApps");
|
||||
settings.beginGroup("Facebook");
|
||||
if(settings.allKeys().contains("ConnectToken")){
|
||||
FacebookManager *fb = FacebookManager::instance();
|
||||
if(fb->loggedIn()){
|
||||
ui.facebookWebView->setHtml("You are connected on Facebook, yey.");
|
||||
ui.fbConnected->show();
|
||||
} else {
|
||||
ui.facebookWebView->setUrl(QUrl(facebookConnectUrl));
|
||||
ui.fbConnected->hide();
|
||||
ui.facebookWebView->setUrl(fb->connectUrl());
|
||||
}
|
||||
|
||||
connect(ui.facebookWebView, &QWebView::urlChanged, this, &PreferencesDialog::facebookLoginResponse);
|
||||
connect(ui.btnDisconnectFacebook, &QPushButton::clicked, this, &PreferencesDialog::facebookDisconnect);
|
||||
fb->setDesiredAlbumName(ui.facebookAlbum->text());
|
||||
connect(ui.facebookAlbum, &QLineEdit::textChanged, fb, &FacebookManager::setDesiredAlbumName);
|
||||
connect(ui.facebookWebView, &QWebView::urlChanged, fb, &FacebookManager::tryLogin);
|
||||
connect(fb, &FacebookManager::justLoggedIn, this, &PreferencesDialog::facebookLoggedIn);
|
||||
connect(ui.btnDisconnectFacebook, &QPushButton::clicked, fb, &FacebookManager::logout);
|
||||
connect(fb, &FacebookManager::justLoggedOut, this, &PreferencesDialog::facebookDisconnect);
|
||||
|
||||
connect(ui.proxyType, SIGNAL(currentIndexChanged(int)), this, SLOT(proxyType_changed(int)));
|
||||
connect(ui.buttonBox, SIGNAL(clicked(QAbstractButton *)), this, SLOT(buttonClicked(QAbstractButton *)));
|
||||
|
@ -67,55 +58,19 @@ PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WindowFlags f) : QDial
|
|||
rememberPrefs();
|
||||
}
|
||||
|
||||
void PreferencesDialog::facebookLoginResponse(const QUrl &url)
|
||||
void PreferencesDialog::facebookLoggedIn()
|
||||
{
|
||||
QString result = url.toString();
|
||||
if (result.contains("access_token")){ // Login Successfull.
|
||||
int from = result.indexOf("access_token=") + strlen("access_token=");
|
||||
int to = result.indexOf("&expires_in");
|
||||
QString securityToken = result.mid(from, to-from);
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup("WebApps");
|
||||
settings.beginGroup("Facebook");
|
||||
settings.setValue("ConnectToken", securityToken);
|
||||
|
||||
QNetworkAccessManager *getUserID = new QNetworkAccessManager();
|
||||
connect(getUserID, &QNetworkAccessManager::finished, this, &PreferencesDialog::facebookGetUserId);
|
||||
getUserID->get(QNetworkRequest(QUrl("https://graph.facebook.com/me?fields=id&access_token=" + securityToken)));
|
||||
ui.facebookWebView->setHtml("We need a better 'you re connected' page. but, YEY. ");
|
||||
ui.fbConnected->show();
|
||||
|
||||
// only enable when we get the reply for the user_id.
|
||||
setDisabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void PreferencesDialog::facebookGetUserId(QNetworkReply *reply)
|
||||
{
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonObject obj = jsonDoc.object();
|
||||
if (obj.keys().contains("id")){
|
||||
QSettings s;
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
s.setValue("UserId", obj.value("id").toVariant());
|
||||
}
|
||||
setEnabled(true);
|
||||
ui.facebookWebView->setHtml("We need a better 'you re connected' page. but, YEY. ");
|
||||
ui.fbConnected->show();
|
||||
}
|
||||
|
||||
void PreferencesDialog::facebookDisconnect()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.beginGroup("WebApps");
|
||||
settings.beginGroup("Facebook");
|
||||
settings.remove("ConnectToken");
|
||||
ui.facebookWebView->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar());
|
||||
ui.facebookWebView->setUrl(QUrl(facebookConnectUrl));
|
||||
ui.fbConnected->hide();
|
||||
ui.facebookWebView->page()->networkAccessManager()->setCookieJar(new QNetworkCookieJar());
|
||||
ui.facebookWebView->setUrl(FacebookManager::instance()->connectUrl());
|
||||
ui.fbConnected->hide();
|
||||
}
|
||||
|
||||
|
||||
#define DANGER_GF (gf > 100) ? "* { color: red; }" : ""
|
||||
void PreferencesDialog::gflowChanged(int gf)
|
||||
{
|
||||
|
@ -211,15 +166,6 @@ void PreferencesDialog::setUiFromPrefs()
|
|||
ui.proxyPassword->setText(prefs.proxy_pass);
|
||||
ui.proxyType->setCurrentIndex(ui.proxyType->findData(prefs.proxy_type));
|
||||
ui.btnUseDefaultFile->setChecked(prefs.use_default_file);
|
||||
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
if(s.allKeys().contains("ConnectToken")){
|
||||
ui.fbConnected->show();
|
||||
} else {
|
||||
ui.fbConnected->hide();
|
||||
}
|
||||
ui.facebookAlbum->setText(s.value("Album", "subsurface").toString());
|
||||
}
|
||||
|
||||
void PreferencesDialog::restorePrefs()
|
||||
|
@ -365,13 +311,6 @@ void PreferencesDialog::syncSettings()
|
|||
s.setValue("proxy_pass", ui.proxyPassword->text());
|
||||
s.endGroup();
|
||||
|
||||
// Facebook
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
s.setValue("Album", ui.facebookAlbum->text());
|
||||
s.endGroup();
|
||||
s.endGroup();
|
||||
|
||||
loadSettings();
|
||||
emit settingsChanged();
|
||||
}
|
||||
|
@ -476,15 +415,6 @@ void PreferencesDialog::loadSettings()
|
|||
GET_TXT("proxy_user", proxy_user);
|
||||
GET_TXT("proxy_pass", proxy_pass);
|
||||
s.endGroup();
|
||||
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
GET_TXT("UserId", facebook.user_id);
|
||||
GET_TXT("ConnectToken", facebook.access_token);
|
||||
GET_TXT("AlbumName", facebook.album_name);
|
||||
s.endGroup();
|
||||
s.endGroup();
|
||||
qDebug() << prefs.facebook.user_id << prefs.facebook.access_token << prefs.facebook.album_name;
|
||||
}
|
||||
|
||||
void PreferencesDialog::buttonClicked(QAbstractButton *button)
|
||||
|
|
|
@ -30,9 +30,8 @@ slots:
|
|||
void gfhighChanged(int gf);
|
||||
void proxyType_changed(int idx);
|
||||
void on_btnUseDefaultFile_toggled(bool toggle);
|
||||
void facebookLoginResponse(const QUrl& url);
|
||||
void facebookLoggedIn();
|
||||
void facebookDisconnect();
|
||||
void facebookGetUserId(QNetworkReply *reply);
|
||||
private:
|
||||
explicit PreferencesDialog(QWidget *parent = 0, Qt::WindowFlags f = 0);
|
||||
void setUiFromPrefs();
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>924</width>
|
||||
<height>718</height>
|
||||
<height>735</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -995,7 +995,11 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="facebookAlbum"/>
|
||||
<widget class="QLineEdit" name="facebookAlbum">
|
||||
<property name="text">
|
||||
<string>subsurface</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
|
|
|
@ -661,49 +661,3 @@ void MultiFilter::closeFilter()
|
|||
MultiFilterSortModel::instance()->clearFilter();
|
||||
hide();
|
||||
}
|
||||
|
||||
FacebookManager::FacebookManager()
|
||||
{
|
||||
}
|
||||
|
||||
bool FacebookManager::checkAlbumExists()
|
||||
{
|
||||
QUrl albumListUrl("https://graph.facebook.com/me/albums?access_token=" + QString(prefs.facebook.access_token));
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager();
|
||||
QNetworkReply *reply = manager->get(QNetworkRequest(albumListUrl));
|
||||
|
||||
// Make this method synchronous.
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
|
||||
QJsonDocument albumsDoc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonArray albumObj = albumsDoc.object().value("data").toArray();
|
||||
foreach(const QJsonValue &v, albumObj){
|
||||
QJsonObject obj = v.toObject();
|
||||
if (obj.value("name").toString() == QString(prefs.facebook.album_name)) {
|
||||
qDebug() << "Album already exists. Try to get the ID in the next commit.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Album doesn't exists, let's create it.";
|
||||
QUrlQuery params;
|
||||
params.addQueryItem("name", prefs.facebook.album_name );
|
||||
params.addQueryItem("description", "Subsurface Album");
|
||||
params.addQueryItem("privacy", "{'value': 'SELF'}");
|
||||
|
||||
QNetworkRequest request(albumListUrl);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
|
||||
reply = manager->post(request, params.query().toLocal8Bit());
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
|
||||
qDebug() << reply->readAll();
|
||||
|
||||
}
|
||||
|
||||
void FacebookManager::createAlbum()
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
@ -214,15 +214,6 @@ private:
|
|||
Ui::FilterWidget ui;
|
||||
};
|
||||
|
||||
class FacebookManager : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FacebookManager();
|
||||
bool checkAlbumExists();
|
||||
void createAlbum();
|
||||
signals:
|
||||
void requestFinished(const QString& result);
|
||||
};
|
||||
bool isGnome3Session();
|
||||
QImage grayImage(const QImage &coloredImg);
|
||||
|
||||
|
|
182
qt-ui/socialnetworks.cpp
Normal file
182
qt-ui/socialnetworks.cpp
Normal file
|
@ -0,0 +1,182 @@
|
|||
#include "socialnetworks.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QUrlQuery>
|
||||
#include <QEventLoop>
|
||||
#include <QSettings>
|
||||
#include <QDebug>
|
||||
|
||||
#include "pref.h"
|
||||
|
||||
#define GET_TXT(name, field) \
|
||||
v = s.value(QString(name)); \
|
||||
if (v.isValid()) \
|
||||
prefs.field = strdup(v.toString().toUtf8().constData()); \
|
||||
else \
|
||||
prefs.field = default_prefs.field
|
||||
|
||||
FacebookManager *FacebookManager::instance()
|
||||
{
|
||||
static FacebookManager *self = new FacebookManager();
|
||||
return self;
|
||||
}
|
||||
|
||||
FacebookManager::FacebookManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
sync();
|
||||
}
|
||||
|
||||
QUrl FacebookManager::connectUrl() {
|
||||
return QUrl("https://www.facebook.com/dialog/oauth?"
|
||||
"client_id=427722490709000"
|
||||
"&redirect_uri=http://www.facebook.com/connect/login_success.html"
|
||||
"&response_type=token"
|
||||
"&scope=publish_actions,user_photos"
|
||||
);
|
||||
}
|
||||
|
||||
bool FacebookManager::loggedIn() {
|
||||
return prefs.facebook.access_token != NULL;
|
||||
}
|
||||
|
||||
void FacebookManager::sync()
|
||||
{
|
||||
qDebug() << "Sync Active";
|
||||
QSettings s;
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
|
||||
QVariant v;
|
||||
GET_TXT("ConnectToken", facebook.access_token);
|
||||
GET_TXT("UserId", facebook.user_id);
|
||||
GET_TXT("AlbumId", facebook.album_id);
|
||||
|
||||
qDebug() << "Connection Token" << prefs.facebook.access_token;
|
||||
qDebug() << "User ID" << prefs.facebook.user_id;
|
||||
qDebug() << "Album ID" << prefs.facebook.album_id;
|
||||
}
|
||||
|
||||
void FacebookManager::tryLogin(const QUrl& loginResponse)
|
||||
{
|
||||
qDebug() << "Trying to Login.";
|
||||
QString result = loginResponse.toString();
|
||||
if (!result.contains("access_token")) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Login Sucessfull";
|
||||
int from = result.indexOf("access_token=") + strlen("access_token=");
|
||||
int to = result.indexOf("&expires_in");
|
||||
QString securityToken = result.mid(from, to-from);
|
||||
|
||||
QSettings settings;
|
||||
settings.beginGroup("WebApps");
|
||||
settings.beginGroup("Facebook");
|
||||
settings.setValue("ConnectToken", securityToken);
|
||||
sync();
|
||||
requestUserId();
|
||||
sync();
|
||||
requestAlbumId();
|
||||
sync();
|
||||
emit justLoggedIn();
|
||||
qDebug() << "End try login";
|
||||
}
|
||||
|
||||
void FacebookManager::logout()
|
||||
{
|
||||
qDebug() << "Logging out";
|
||||
QSettings settings;
|
||||
settings.beginGroup("WebApps");
|
||||
settings.beginGroup("Facebook");
|
||||
settings.remove("ConnectToken");
|
||||
settings.remove("UserId");
|
||||
settings.remove("AlbumId");
|
||||
sync();
|
||||
emit justLoggedOut();
|
||||
}
|
||||
|
||||
void FacebookManager::requestAlbumId()
|
||||
{
|
||||
qDebug() << "Requesting Album ID";
|
||||
|
||||
QUrl albumListUrl("https://graph.facebook.com/me/albums?access_token=" + QString(prefs.facebook.access_token));
|
||||
QNetworkAccessManager *manager = new QNetworkAccessManager();
|
||||
QNetworkReply *reply = manager->get(QNetworkRequest(albumListUrl));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
|
||||
QJsonDocument albumsDoc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonArray albumObj = albumsDoc.object().value("data").toArray();
|
||||
foreach(const QJsonValue &v, albumObj){
|
||||
QJsonObject obj = v.toObject();
|
||||
if (obj.value("name").toString() == albumName) {
|
||||
qDebug() << "Album already Exists, using it's id";
|
||||
s.setValue("AlbumId", obj.value("id").toString());
|
||||
qDebug() << "Got album ID";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Album doesn't exists, creating one.";
|
||||
QUrlQuery params;
|
||||
qDebug() << "TRYING TO SET NAME" << albumName;
|
||||
params.addQueryItem("name", albumName );
|
||||
params.addQueryItem("description", "Subsurface Album");
|
||||
params.addQueryItem("privacy", "{'value': 'SELF'}");
|
||||
|
||||
QNetworkRequest request(albumListUrl);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
|
||||
reply = manager->post(request, params.query().toLocal8Bit());
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
|
||||
albumsDoc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonObject album = albumsDoc.object();
|
||||
if (album.contains("id")) {
|
||||
s.setValue("AlbumId", album.value("id").toString());
|
||||
qDebug() << "Got album ID";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Error getting album id" << album;
|
||||
}
|
||||
|
||||
void FacebookManager::requestUserId()
|
||||
{
|
||||
qDebug() << "trying to get user Id";
|
||||
QUrl userIdRequest("https://graph.facebook.com/me?fields=id&access_token=" + QString(prefs.facebook.access_token));
|
||||
QNetworkAccessManager *getUserID = new QNetworkAccessManager();
|
||||
QNetworkReply *reply = getUserID->get(QNetworkRequest(userIdRequest));
|
||||
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonObject obj = jsonDoc.object();
|
||||
if (obj.keys().contains("id")){
|
||||
QSettings s;
|
||||
s.beginGroup("WebApps");
|
||||
s.beginGroup("Facebook");
|
||||
s.setValue("UserId", obj.value("id").toVariant());
|
||||
qDebug() << "Got user id.";
|
||||
return;
|
||||
}
|
||||
qDebug() << "error getting user id" << obj;
|
||||
}
|
||||
|
||||
void FacebookManager::setDesiredAlbumName(const QString& a)
|
||||
{
|
||||
albumName = a;
|
||||
}
|
31
qt-ui/socialnetworks.h
Normal file
31
qt-ui/socialnetworks.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
#ifndef FACEBOOKMANAGER_H
|
||||
#define FACEBOOKMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
class FacebookManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static FacebookManager *instance();
|
||||
void requestAlbumId();
|
||||
void requestUserId();
|
||||
void sync();
|
||||
QUrl connectUrl();
|
||||
bool loggedIn();
|
||||
signals:
|
||||
void justLoggedIn();
|
||||
void justLoggedOut();
|
||||
|
||||
public slots:
|
||||
void tryLogin(const QUrl& loginResponse);
|
||||
void logout();
|
||||
void setDesiredAlbumName(const QString& albumName);
|
||||
|
||||
private:
|
||||
explicit FacebookManager(QObject *parent = 0);
|
||||
QString albumName;
|
||||
};
|
||||
|
||||
#endif // FACEBOOKMANAGER_H
|
|
@ -103,7 +103,8 @@ HEADERS = \
|
|||
qt-ui/statistics/statisticsbar.h \
|
||||
qt-ui/statistics/yearstatistics.h \
|
||||
qt-ui/diveshareexportdialog.h \
|
||||
qt-ui/filtermodels.h
|
||||
qt-ui/filtermodels.h \
|
||||
qt-ui/socialnetworks.h
|
||||
|
||||
android: HEADERS -= \
|
||||
qt-ui/usermanual.h \
|
||||
|
@ -197,7 +198,8 @@ SOURCES = \
|
|||
qt-ui/statistics/statisticsbar.cpp \
|
||||
qt-ui/statistics/monthstatistics.cpp \
|
||||
qt-ui/diveshareexportdialog.cpp \
|
||||
qt-ui/filtermodels.cpp
|
||||
qt-ui/filtermodels.cpp \
|
||||
qt-ui/socialnetworks.cpp
|
||||
|
||||
android: SOURCES += android.cpp
|
||||
else: win32: SOURCES += windows.c
|
||||
|
|
|
@ -50,9 +50,9 @@ struct preferences default_prefs = {
|
|||
.show_pictures_in_profile = true,
|
||||
.tankbar = false,
|
||||
.facebook = {
|
||||
.user_id = "",
|
||||
.album_name = "subsurface",
|
||||
.access_token = ""
|
||||
.user_id = NULL,
|
||||
.album_id = NULL,
|
||||
.access_token = NULL
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue